@dokebi/cli 0.4.2 → 0.4.27
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/AI.md +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +638 -37
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +15 -0
- package/dist/check/shared-state-rules.js +117 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +381 -36
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +20 -2
- package/dist/install-module.js +45 -8
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +17 -2
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +40 -4
- package/dist/npm-client.js +152 -15
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +195 -16
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +497 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3006 -333
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// The background timer rule of `dokebi check` (D-0105 §1), over the TypeScript AST:
|
|
2
|
+
// background-timer (error) server code under src/ (not views/, not tests/) that runs periodic
|
|
3
|
+
// work on its own timer instead of @dokebi/scheduler and @dokebi/job:
|
|
4
|
+
// - any setInterval (global, node:timers, or the async iterator of
|
|
5
|
+
// node:timers/promises),
|
|
6
|
+
// - a setTimeout or setImmediate whose callback reschedules it: the callback
|
|
7
|
+
// reaches (through the functions and methods of the same file) a function
|
|
8
|
+
// that encloses the call (`const tick = () => { …; setTimeout(tick, ms) }`,
|
|
9
|
+
// `#schedule() { setTimeout(() => this.#tick()) }` with #tick calling
|
|
10
|
+
// #schedule again).
|
|
11
|
+
// Not reported: one-shot delays and time limits (a setTimeout whose callback
|
|
12
|
+
// does not come back to it: `setTimeout(() => controller.abort(), ms)`,
|
|
13
|
+
// `new Promise((resolve) => setTimeout(resolve, ms))`), every awaited sleep
|
|
14
|
+
// of node:timers/promises (`await setTimeout(ms, undefined, { signal })`,
|
|
15
|
+
// which a job handler uses between bounded batches), and timers owned by one
|
|
16
|
+
// connection: inside a function that takes a request, reply, socket or stream
|
|
17
|
+
// parameter, or a handler passed to a route method (get, post, …, route),
|
|
18
|
+
// unless the timer is stored where it outlives that function (`this.ticker =
|
|
19
|
+
// setInterval(…)`, a variable declared outside it: R4 Codex T2 kept one
|
|
20
|
+
// worker-wide ticker started by the first stream request).
|
|
21
|
+
// A `// dokebi-check: allow background-timer <reason>` comment keeps a deliberate one (per-process
|
|
22
|
+
// upkeep of in-memory state that no job can reach).
|
|
23
|
+
import { field, nodes, property, } from './typescript.js';
|
|
24
|
+
const CALLBACK_TIMERS = new Set(['setTimeout', 'setInterval', 'setImmediate']);
|
|
25
|
+
const PROMISE_MODULES = new Set(['node:timers/promises', 'timers/promises']);
|
|
26
|
+
const CALLBACK_MODULES = new Set(['node:timers', 'timers']);
|
|
27
|
+
/** Parameters that tie a function to one connection: its timers end with the connection. */
|
|
28
|
+
const CONNECTION_PARAMETERS = new Set([
|
|
29
|
+
'request',
|
|
30
|
+
'req',
|
|
31
|
+
'reply',
|
|
32
|
+
'res',
|
|
33
|
+
'response',
|
|
34
|
+
'socket',
|
|
35
|
+
'ws',
|
|
36
|
+
'websocket',
|
|
37
|
+
'stream',
|
|
38
|
+
]);
|
|
39
|
+
/** Route registration methods: a handler passed to one runs per request. */
|
|
40
|
+
const ROUTE_METHODS = new Set([
|
|
41
|
+
'get',
|
|
42
|
+
'post',
|
|
43
|
+
'put',
|
|
44
|
+
'patch',
|
|
45
|
+
'delete',
|
|
46
|
+
'head',
|
|
47
|
+
'options',
|
|
48
|
+
'all',
|
|
49
|
+
'route',
|
|
50
|
+
]);
|
|
51
|
+
/** Paths the rule reads: server code under src/, without views (browser code) and tests. */
|
|
52
|
+
export function isServerCode(path) {
|
|
53
|
+
return path.startsWith('src/') && !/(^|\/)(views|public)\//.test(path);
|
|
54
|
+
}
|
|
55
|
+
export const TIMER_FIX = 'run periodic work as a job: register the handler in the setup of the module that owns it (context.getDependency(job).queue(<queue>).handle(<type>, async (payload, { signal }) => …), src/shared/modules/<module>/module.ts) and add its entry to options.dokebi-scheduler.schedules in dokebi.yaml (npx dokebi add @dokebi/job, then @dokebi/scheduler); inside one job use await setTimeout(ms, undefined, { signal }) from node:timers/promises between bounded batches. Per-process upkeep of in-memory state that no job can reach: // dokebi-check: allow background-timer <reason>';
|
|
56
|
+
export const POLLING_FIX = 'move the loop to background work the framework runs: an @dokebi/outbox handler (one coalesced item per owner and window with a dedupeKey, its handler doing one bounded batch through context.db and enqueuing the next while work remains) or a @dokebi/job handler with an options.dokebi-scheduler.schedules entry; a dokebi-check marker does not silence this rule';
|
|
57
|
+
export function timerRules(file, kinds, report) {
|
|
58
|
+
const text = file.text;
|
|
59
|
+
// Most files have no timer at all: skip the walks.
|
|
60
|
+
if (!/\bset(?:Timeout|Interval|Immediate)\b/.test(text))
|
|
61
|
+
return;
|
|
62
|
+
const name = (node) => {
|
|
63
|
+
if (!node)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (node.kind === kinds.Identifier || node.kind === kinds.PrivateIdentifier)
|
|
66
|
+
return String(property(node, 'text'));
|
|
67
|
+
return undefined;
|
|
68
|
+
};
|
|
69
|
+
const stringValue = (node) => node && (node.kind === kinds.StringLiteral || node.kind === kinds.NoSubstitutionTemplateLiteral)
|
|
70
|
+
? String(property(node, 'text'))
|
|
71
|
+
: undefined;
|
|
72
|
+
// Imports of the timer modules: local name -> imported name, and namespace names.
|
|
73
|
+
const promiseNames = new Map();
|
|
74
|
+
const callbackNames = new Map();
|
|
75
|
+
const promiseSpaces = new Set();
|
|
76
|
+
const callbackSpaces = new Set(['globalThis']);
|
|
77
|
+
for (const statement of nodes(file, 'statements')) {
|
|
78
|
+
if (statement.kind !== kinds.ImportDeclaration)
|
|
79
|
+
continue;
|
|
80
|
+
const from = stringValue(field(statement, 'moduleSpecifier')) ?? '';
|
|
81
|
+
const promise = PROMISE_MODULES.has(from);
|
|
82
|
+
if (!promise && !CALLBACK_MODULES.has(from))
|
|
83
|
+
continue;
|
|
84
|
+
const clause = field(statement, 'importClause');
|
|
85
|
+
const bindings = clause && field(clause, 'namedBindings');
|
|
86
|
+
const spaces = promise ? promiseSpaces : callbackSpaces;
|
|
87
|
+
const own = name(clause && field(clause, 'name'));
|
|
88
|
+
if (own)
|
|
89
|
+
spaces.add(own);
|
|
90
|
+
if (bindings?.kind === kinds.NamespaceImport) {
|
|
91
|
+
const space = name(field(bindings, 'name'));
|
|
92
|
+
if (space)
|
|
93
|
+
spaces.add(space);
|
|
94
|
+
}
|
|
95
|
+
for (const element of bindings ? nodes(bindings, 'elements') : []) {
|
|
96
|
+
const local = name(field(element, 'name'));
|
|
97
|
+
const imported = name(field(element, 'propertyName')) ?? local;
|
|
98
|
+
if (local && imported)
|
|
99
|
+
(promise ? promiseNames : callbackNames).set(local, imported);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const functional = (node) => node.kind === kinds.ArrowFunction ||
|
|
103
|
+
node.kind === kinds.FunctionExpression ||
|
|
104
|
+
node.kind === kinds.FunctionDeclaration ||
|
|
105
|
+
node.kind === kinds.MethodDeclaration ||
|
|
106
|
+
node.kind === kinds.Constructor ||
|
|
107
|
+
node.kind === kinds.GetAccessor ||
|
|
108
|
+
node.kind === kinds.SetAccessor;
|
|
109
|
+
/** `this.x` / `this.#x` -> `x` / `#x`; an identifier -> its text. */
|
|
110
|
+
const reference = (node) => {
|
|
111
|
+
if (!node)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (node.kind === kinds.Identifier)
|
|
114
|
+
return name(node);
|
|
115
|
+
if (node.kind === kinds.PropertyAccessExpression &&
|
|
116
|
+
field(node, 'expression')?.kind === kinds.ThisKeyword)
|
|
117
|
+
return name(field(node, 'name'));
|
|
118
|
+
return undefined;
|
|
119
|
+
};
|
|
120
|
+
/** The name a function is known by: its own, or the variable, property or field it is put in. */
|
|
121
|
+
const functionName = (node) => {
|
|
122
|
+
const own = name(field(node, 'name'));
|
|
123
|
+
if (own)
|
|
124
|
+
return own;
|
|
125
|
+
const parent = node.parent;
|
|
126
|
+
if (!parent)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (parent.kind === kinds.VariableDeclaration ||
|
|
129
|
+
parent.kind === kinds.PropertyAssignment ||
|
|
130
|
+
parent.kind === kinds.PropertyDeclaration)
|
|
131
|
+
return name(field(parent, 'name'));
|
|
132
|
+
if (parent.kind === kinds.BinaryExpression && field(parent, 'right') === node)
|
|
133
|
+
return reference(field(parent, 'left'));
|
|
134
|
+
return undefined;
|
|
135
|
+
};
|
|
136
|
+
// Every named function of the file, and what each refers to (calls and functions passed on).
|
|
137
|
+
const bodies = new Map();
|
|
138
|
+
const collect = (node) => {
|
|
139
|
+
if (functional(node)) {
|
|
140
|
+
const own = functionName(node);
|
|
141
|
+
if (own)
|
|
142
|
+
bodies.set(own, [...(bodies.get(own) ?? []), node]);
|
|
143
|
+
}
|
|
144
|
+
node.forEachChild(collect);
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
collect(file);
|
|
148
|
+
const referenced = new Map();
|
|
149
|
+
const references = (root) => {
|
|
150
|
+
const cached = referenced.get(root);
|
|
151
|
+
if (cached)
|
|
152
|
+
return cached;
|
|
153
|
+
const found = new Set();
|
|
154
|
+
const direct = reference(root);
|
|
155
|
+
if (direct)
|
|
156
|
+
found.add(direct);
|
|
157
|
+
const walk = (node) => {
|
|
158
|
+
if (node.kind === kinds.CallExpression || node.kind === kinds.NewExpression) {
|
|
159
|
+
const callee = reference(field(node, 'expression'));
|
|
160
|
+
if (callee)
|
|
161
|
+
found.add(callee);
|
|
162
|
+
for (const argument of nodes(node, 'arguments')) {
|
|
163
|
+
const passed = reference(argument);
|
|
164
|
+
if (passed)
|
|
165
|
+
found.add(passed);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
node.forEachChild(walk);
|
|
169
|
+
return undefined;
|
|
170
|
+
};
|
|
171
|
+
walk(root);
|
|
172
|
+
referenced.set(root, found);
|
|
173
|
+
return found;
|
|
174
|
+
};
|
|
175
|
+
/** Whether running `callback` comes back to one of `targets` through the file's functions. */
|
|
176
|
+
const reaches = (callback, targets) => {
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
const queue = [...references(callback)];
|
|
179
|
+
while (queue.length) {
|
|
180
|
+
const next = queue.pop();
|
|
181
|
+
if (targets.has(next))
|
|
182
|
+
return true;
|
|
183
|
+
if (seen.has(next))
|
|
184
|
+
continue;
|
|
185
|
+
seen.add(next);
|
|
186
|
+
for (const body of bodies.get(next) ?? [])
|
|
187
|
+
queue.push(...references(body));
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
/** The timer a call starts: `interval`, `once` (setTimeout, setImmediate) or none. */
|
|
192
|
+
const timerOf = (call) => {
|
|
193
|
+
const callee = field(call, 'expression');
|
|
194
|
+
let imported;
|
|
195
|
+
let promise = false;
|
|
196
|
+
if (callee?.kind === kinds.Identifier) {
|
|
197
|
+
const local = name(callee);
|
|
198
|
+
if (promiseNames.has(local)) {
|
|
199
|
+
imported = promiseNames.get(local);
|
|
200
|
+
promise = true;
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
imported = callbackNames.get(local) ?? local;
|
|
204
|
+
}
|
|
205
|
+
else if (callee?.kind === kinds.PropertyAccessExpression) {
|
|
206
|
+
const space = name(field(callee, 'expression'));
|
|
207
|
+
if (!space)
|
|
208
|
+
return undefined;
|
|
209
|
+
imported = name(field(callee, 'name'));
|
|
210
|
+
if (promiseSpaces.has(space))
|
|
211
|
+
promise = true;
|
|
212
|
+
else if (!callbackSpaces.has(space))
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (!imported || !CALLBACK_TIMERS.has(imported))
|
|
216
|
+
return undefined;
|
|
217
|
+
// Awaited sleeps of node:timers/promises end by themselves; its setInterval is a loop.
|
|
218
|
+
if (promise)
|
|
219
|
+
return imported === 'setInterval' ? 'interval' : undefined;
|
|
220
|
+
return imported === 'setInterval' ? 'interval' : 'once';
|
|
221
|
+
};
|
|
222
|
+
/** The innermost function around `node` that serves one request or connection. */
|
|
223
|
+
const connectionFunction = (node) => {
|
|
224
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
225
|
+
if (!functional(current))
|
|
226
|
+
continue;
|
|
227
|
+
if (nodes(current, 'parameters').some((parameter) => {
|
|
228
|
+
const own = name(field(parameter, 'name'));
|
|
229
|
+
return own !== undefined && CONNECTION_PARAMETERS.has(own);
|
|
230
|
+
}))
|
|
231
|
+
return current;
|
|
232
|
+
const call = current.parent;
|
|
233
|
+
if (call?.kind === kinds.CallExpression && nodes(call, 'arguments').includes(current)) {
|
|
234
|
+
const callee = field(call, 'expression');
|
|
235
|
+
if (callee?.kind === kinds.PropertyAccessExpression &&
|
|
236
|
+
ROUTE_METHODS.has(name(field(callee, 'name')) ?? ''))
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
};
|
|
242
|
+
/** Whether `scope` declares `local` itself (a parameter or a variable inside it). */
|
|
243
|
+
const declares = (scope, local) => {
|
|
244
|
+
let found = false;
|
|
245
|
+
const walk = (child) => {
|
|
246
|
+
if ((child.kind === kinds.Parameter ||
|
|
247
|
+
child.kind === kinds.VariableDeclaration ||
|
|
248
|
+
child.kind === kinds.BindingElement) &&
|
|
249
|
+
name(field(child, 'name')) === local)
|
|
250
|
+
found = true;
|
|
251
|
+
if (!found)
|
|
252
|
+
child.forEachChild(walk);
|
|
253
|
+
return undefined;
|
|
254
|
+
};
|
|
255
|
+
scope.forEachChild(walk);
|
|
256
|
+
return found;
|
|
257
|
+
};
|
|
258
|
+
const ASSIGNMENTS = new Set([
|
|
259
|
+
kinds.EqualsToken,
|
|
260
|
+
kinds.QuestionQuestionEqualsToken,
|
|
261
|
+
kinds.BarBarEqualsToken,
|
|
262
|
+
]);
|
|
263
|
+
/** The timer `call` is assigned to `this.…` or to a variable `scope` does not declare. */
|
|
264
|
+
const outlives = (call, scope) => {
|
|
265
|
+
let value = call;
|
|
266
|
+
let parent = value.parent;
|
|
267
|
+
while (parent &&
|
|
268
|
+
(parent.kind === kinds.ParenthesizedExpression ||
|
|
269
|
+
parent.kind === kinds.AsExpression ||
|
|
270
|
+
parent.kind === kinds.NonNullExpression ||
|
|
271
|
+
parent.kind === kinds.TypeAssertionExpression)) {
|
|
272
|
+
value = parent;
|
|
273
|
+
parent = parent.parent;
|
|
274
|
+
}
|
|
275
|
+
if (parent?.kind !== kinds.BinaryExpression || field(parent, 'right') !== value)
|
|
276
|
+
return false;
|
|
277
|
+
const operator = field(parent, 'operatorToken');
|
|
278
|
+
if (!operator || !ASSIGNMENTS.has(operator.kind))
|
|
279
|
+
return false;
|
|
280
|
+
let target = field(parent, 'left');
|
|
281
|
+
while (target &&
|
|
282
|
+
(target.kind === kinds.PropertyAccessExpression ||
|
|
283
|
+
target.kind === kinds.ElementAccessExpression ||
|
|
284
|
+
target.kind === kinds.ParenthesizedExpression ||
|
|
285
|
+
target.kind === kinds.NonNullExpression))
|
|
286
|
+
target = field(target, 'expression');
|
|
287
|
+
if (target?.kind === kinds.ThisKeyword)
|
|
288
|
+
return true;
|
|
289
|
+
const local = name(target);
|
|
290
|
+
return local !== undefined && !declares(scope, local);
|
|
291
|
+
};
|
|
292
|
+
/** A timer owned by one request or connection: ends with it, never stored beyond it. */
|
|
293
|
+
const connectionScope = (node) => {
|
|
294
|
+
const scope = connectionFunction(node);
|
|
295
|
+
return scope !== undefined && !(node.kind === kinds.CallExpression && outlives(node, scope));
|
|
296
|
+
};
|
|
297
|
+
/** The names of the functions around `node`, innermost first. */
|
|
298
|
+
const enclosing = (node) => {
|
|
299
|
+
const found = new Set();
|
|
300
|
+
for (let current = node.parent; current; current = current.parent)
|
|
301
|
+
if (functional(current)) {
|
|
302
|
+
const own = functionName(current);
|
|
303
|
+
if (own)
|
|
304
|
+
found.add(own);
|
|
305
|
+
}
|
|
306
|
+
return found;
|
|
307
|
+
};
|
|
308
|
+
const callee = (call) => {
|
|
309
|
+
const target = field(call, 'expression');
|
|
310
|
+
return target ? text.slice(target.getStart(), target.end) : 'setTimeout';
|
|
311
|
+
};
|
|
312
|
+
// A loop that sleeps inside a task the app starts and never awaits (M33: `task = (async () => {
|
|
313
|
+
// while (!stopped) { await step(); await new Promise((r) => setTimeout(r, ms)); } })()` in a
|
|
314
|
+
// module's start). Only background tasks: the same loop in a job handler or in a function its
|
|
315
|
+
// caller awaits is a bounded wait between batches, which the rule allows.
|
|
316
|
+
const LOOPS = new Set([
|
|
317
|
+
kinds.WhileStatement,
|
|
318
|
+
kinds.DoStatement,
|
|
319
|
+
kinds.ForStatement,
|
|
320
|
+
kinds.ForOfStatement,
|
|
321
|
+
kinds.ForInStatement,
|
|
322
|
+
]);
|
|
323
|
+
const unwrap = (node) => {
|
|
324
|
+
let current = node;
|
|
325
|
+
while (current?.kind === kinds.ParenthesizedExpression)
|
|
326
|
+
current = field(current, 'expression');
|
|
327
|
+
return current;
|
|
328
|
+
};
|
|
329
|
+
/** The parent of `node`, past parentheses. */
|
|
330
|
+
const outer = (node) => {
|
|
331
|
+
let current = node.parent;
|
|
332
|
+
while (current?.kind === kinds.ParenthesizedExpression)
|
|
333
|
+
current = current.parent;
|
|
334
|
+
return current;
|
|
335
|
+
};
|
|
336
|
+
/** `new Promise((resolve) => … setTimeout(…) …)`: a sleep written by hand. */
|
|
337
|
+
const promiseSleep = (node) => {
|
|
338
|
+
if (node?.kind !== kinds.NewExpression || name(field(node, 'expression')) !== 'Promise')
|
|
339
|
+
return false;
|
|
340
|
+
let found = false;
|
|
341
|
+
const walk = (child) => {
|
|
342
|
+
if (child.kind === kinds.CallExpression && timerOf(child) === 'once')
|
|
343
|
+
found = true;
|
|
344
|
+
if (!found)
|
|
345
|
+
child.forEachChild(walk);
|
|
346
|
+
return undefined;
|
|
347
|
+
};
|
|
348
|
+
for (const argument of nodes(node, 'arguments'))
|
|
349
|
+
walk(argument);
|
|
350
|
+
return found;
|
|
351
|
+
};
|
|
352
|
+
/** The awaited expression sleeps: a hand-written sleep, timers/promises, or a helper of this file. */
|
|
353
|
+
const sleeps = (expression, seen = new Set()) => {
|
|
354
|
+
const value = unwrap(expression);
|
|
355
|
+
if (!value)
|
|
356
|
+
return false;
|
|
357
|
+
if (promiseSleep(value))
|
|
358
|
+
return true;
|
|
359
|
+
if (value.kind !== kinds.CallExpression)
|
|
360
|
+
return false;
|
|
361
|
+
const target = field(value, 'expression');
|
|
362
|
+
if (target?.kind === kinds.Identifier) {
|
|
363
|
+
const local = name(target);
|
|
364
|
+
const imported = promiseNames.get(local);
|
|
365
|
+
if (imported === 'setTimeout' || imported === 'setImmediate')
|
|
366
|
+
return true;
|
|
367
|
+
if (seen.has(local))
|
|
368
|
+
return false;
|
|
369
|
+
seen.add(local);
|
|
370
|
+
return (bodies.get(local) ?? []).some((body) => {
|
|
371
|
+
const result = field(body, 'body');
|
|
372
|
+
if (result && result.kind !== kinds.Block)
|
|
373
|
+
return sleeps(result, seen);
|
|
374
|
+
let found = false;
|
|
375
|
+
const walk = (child) => {
|
|
376
|
+
if (child.kind === kinds.ReturnStatement && sleeps(field(child, 'expression'), seen))
|
|
377
|
+
found = true;
|
|
378
|
+
if (!found && !functional(child))
|
|
379
|
+
child.forEachChild(walk);
|
|
380
|
+
return undefined;
|
|
381
|
+
};
|
|
382
|
+
if (result)
|
|
383
|
+
result.forEachChild(walk);
|
|
384
|
+
return found;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (target?.kind === kinds.PropertyAccessExpression) {
|
|
388
|
+
const space = name(field(target, 'expression'));
|
|
389
|
+
const member = name(field(target, 'name'));
|
|
390
|
+
return (space !== undefined &&
|
|
391
|
+
promiseSpaces.has(space) &&
|
|
392
|
+
(member === 'setTimeout' || member === 'setImmediate'));
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
};
|
|
396
|
+
/** Calls of `fn` by name in this file that nobody awaits or returns. */
|
|
397
|
+
const startedUnawaited = (own) => {
|
|
398
|
+
let found = false;
|
|
399
|
+
const walk = (node) => {
|
|
400
|
+
if (node.kind === kinds.CallExpression && reference(field(node, 'expression')) === own) {
|
|
401
|
+
const parent = outer(node);
|
|
402
|
+
const awaited = parent?.kind === kinds.AwaitExpression ||
|
|
403
|
+
parent?.kind === kinds.ReturnStatement ||
|
|
404
|
+
(parent?.kind === kinds.ArrowFunction && field(parent, 'body') === node);
|
|
405
|
+
if (!awaited)
|
|
406
|
+
found = true;
|
|
407
|
+
}
|
|
408
|
+
if (!found)
|
|
409
|
+
node.forEachChild(walk);
|
|
410
|
+
return undefined;
|
|
411
|
+
};
|
|
412
|
+
walk(file);
|
|
413
|
+
return found;
|
|
414
|
+
};
|
|
415
|
+
/** A function the app starts in the background: an IIFE, or a function called without await. */
|
|
416
|
+
const background = (fn) => {
|
|
417
|
+
let current = fn;
|
|
418
|
+
while (current.parent?.kind === kinds.ParenthesizedExpression)
|
|
419
|
+
current = current.parent;
|
|
420
|
+
const parent = current.parent;
|
|
421
|
+
if (parent?.kind === kinds.CallExpression && field(parent, 'expression') === current)
|
|
422
|
+
return true;
|
|
423
|
+
const own = functionName(fn);
|
|
424
|
+
return own !== undefined && startedUnawaited(own);
|
|
425
|
+
};
|
|
426
|
+
/** The function a node is in (the nearest one). */
|
|
427
|
+
const owner = (node) => {
|
|
428
|
+
for (let current = node.parent; current; current = current.parent)
|
|
429
|
+
if (functional(current))
|
|
430
|
+
return current;
|
|
431
|
+
return undefined;
|
|
432
|
+
};
|
|
433
|
+
/** The loop sleeps: an awaited sleep in its body or condition, not inside a nested function. */
|
|
434
|
+
const loopSleeps = (loop) => {
|
|
435
|
+
let found = false;
|
|
436
|
+
const walk = (node) => {
|
|
437
|
+
if (functional(node))
|
|
438
|
+
return undefined;
|
|
439
|
+
if (node.kind === kinds.AwaitExpression && sleeps(field(node, 'expression')))
|
|
440
|
+
found = true;
|
|
441
|
+
if (!found)
|
|
442
|
+
node.forEachChild(walk);
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
loop.forEachChild(walk);
|
|
446
|
+
return found;
|
|
447
|
+
};
|
|
448
|
+
// M36: builders also put database work (claims, expiry sweeps, folds) on setInterval under
|
|
449
|
+
// an allow marker. A timer whose callback reaches database work is reported as polling-loop,
|
|
450
|
+
// which no marker silences; per-process upkeep of other state keeps background-timer.
|
|
451
|
+
const DB_RECEIVER = /^(?:db|pool|client|tx|trx|knex|sql|outbox|models?|repo|repository|dao|.*(?:Model|Repository|Repo|Dao|Pool))$/;
|
|
452
|
+
const DB_METHOD = new Set(['query', 'execute', 'transaction']);
|
|
453
|
+
const receivers = (target) => {
|
|
454
|
+
const names = [];
|
|
455
|
+
for (let current = target; current;) {
|
|
456
|
+
if (current.kind === kinds.PropertyAccessExpression) {
|
|
457
|
+
const member = name(field(current, 'name'));
|
|
458
|
+
if (member)
|
|
459
|
+
names.push(member);
|
|
460
|
+
current = field(current, 'expression');
|
|
461
|
+
}
|
|
462
|
+
else if (current.kind === kinds.CallExpression)
|
|
463
|
+
current = field(current, 'expression');
|
|
464
|
+
else {
|
|
465
|
+
const own = name(current);
|
|
466
|
+
if (own)
|
|
467
|
+
names.push(own);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return names;
|
|
472
|
+
};
|
|
473
|
+
/** The region (a callback or a function and what it calls in this file) reaches database work. */
|
|
474
|
+
const touchesDatabase = (region, seen = new Set()) => {
|
|
475
|
+
if (!region)
|
|
476
|
+
return false;
|
|
477
|
+
let found = false;
|
|
478
|
+
const walk = (node) => {
|
|
479
|
+
if (found)
|
|
480
|
+
return undefined;
|
|
481
|
+
if (node.kind === kinds.CallExpression) {
|
|
482
|
+
const target = field(node, 'expression');
|
|
483
|
+
if (target?.kind === kinds.PropertyAccessExpression) {
|
|
484
|
+
const method = name(field(target, 'name'));
|
|
485
|
+
const chain = receivers(field(target, 'expression'));
|
|
486
|
+
if ((method && DB_METHOD.has(method)) || chain.some((item) => DB_RECEIVER.test(item)))
|
|
487
|
+
found = true;
|
|
488
|
+
}
|
|
489
|
+
const local = reference(target);
|
|
490
|
+
if (!found && local && !seen.has(local)) {
|
|
491
|
+
seen.add(local);
|
|
492
|
+
for (const body of bodies.get(local) ?? [])
|
|
493
|
+
if (touchesDatabase(body, seen))
|
|
494
|
+
found = true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!found)
|
|
498
|
+
node.forEachChild(walk);
|
|
499
|
+
return undefined;
|
|
500
|
+
};
|
|
501
|
+
const direct = reference(region);
|
|
502
|
+
if (direct && !seen.has(direct)) {
|
|
503
|
+
seen.add(direct);
|
|
504
|
+
for (const body of bodies.get(direct) ?? [])
|
|
505
|
+
if (touchesDatabase(body, seen))
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
walk(region);
|
|
509
|
+
return found;
|
|
510
|
+
};
|
|
511
|
+
const DB_TIMER = 'runs database work on a timer of every app worker (claims, sweeps, folds): nothing claims the work across workers, records a failure or limits its time, and a restart loses where it was. An allow marker does not cover it: per-process upkeep of in-memory state keeps background-timer, database work is background work (M36)';
|
|
512
|
+
const visit = (node) => {
|
|
513
|
+
if (LOOPS.has(node.kind) && !connectionScope(node)) {
|
|
514
|
+
const fn = owner(node);
|
|
515
|
+
if (fn && loopSleeps(node) && background(fn))
|
|
516
|
+
report(node, 'a loop that sleeps inside a task started in the background (a polling loop): periodic work outside @dokebi/scheduler and @dokebi/job runs in every app worker at once, nothing claims the work, records a failure or limits its time, and a restart loses where it was. An allow marker does not cover it: per-process upkeep uses a timer, never a sleeping loop (M34, M35)', POLLING_FIX, 'polling-loop');
|
|
517
|
+
}
|
|
518
|
+
if (node.kind === kinds.CallExpression) {
|
|
519
|
+
const timer = timerOf(node);
|
|
520
|
+
if (timer && !connectionScope(node)) {
|
|
521
|
+
const callback = nodes(node, 'arguments')[0];
|
|
522
|
+
if (timer === 'interval') {
|
|
523
|
+
if (touchesDatabase(callback))
|
|
524
|
+
report(node, `setInterval ${DB_TIMER}`, POLLING_FIX, 'polling-loop');
|
|
525
|
+
else
|
|
526
|
+
report(node, 'setInterval runs periodic work outside @dokebi/scheduler and @dokebi/job: it runs in every app worker at once, nothing claims the work, records a failure or limits its time, and a restart loses where it was', TIMER_FIX);
|
|
527
|
+
}
|
|
528
|
+
else if (callback) {
|
|
529
|
+
const around = enclosing(node);
|
|
530
|
+
if (around.size && reaches(callback, around)) {
|
|
531
|
+
if (touchesDatabase(callback) || touchesDatabase(owner(node)))
|
|
532
|
+
report(node, `${callee(node)} ${DB_TIMER}`, POLLING_FIX, 'polling-loop');
|
|
533
|
+
else
|
|
534
|
+
report(node, `${callee(node)} reschedules itself (a background loop): periodic work outside @dokebi/scheduler and @dokebi/job runs in every app worker at once, nothing claims the work, records a failure or limits its time, and a restart loses where it was`, TIMER_FIX);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
node.forEachChild(visit);
|
|
540
|
+
return undefined;
|
|
541
|
+
};
|
|
542
|
+
file.forEachChild(visit);
|
|
543
|
+
}
|
|
@@ -22,7 +22,7 @@ export declare function nodes(node: TsNode, name: string): readonly TsNode[];
|
|
|
22
22
|
/** A primitive property (`text` of identifiers and literals, `isExportEquals`, …). */
|
|
23
23
|
export declare function property(node: TsNode, name: string): unknown;
|
|
24
24
|
/** SyntaxKind names the rules use; each must exist in the loaded TypeScript. */
|
|
25
|
-
declare const KIND_NAMES: readonly ['ArrowFunction', 'Block', 'CallExpression', 'CatchClause', 'Constructor', '
|
|
25
|
+
declare const KIND_NAMES: readonly ['ArrayLiteralExpression', 'ArrowFunction', 'AsExpression', 'AwaitExpression', 'BarBarEqualsToken', 'BarBarToken', 'BinaryExpression', 'BindingElement', 'Block', 'BreakStatement', 'CallExpression', 'CatchClause', 'ClassDeclaration', 'ClassExpression', 'ConditionalExpression', 'Constructor', 'ContinueStatement', 'DeclareKeyword', 'Decorator', 'DoStatement', 'ElementAccessExpression', 'EnumDeclaration', 'EqualsEqualsEqualsToken', 'EqualsEqualsToken', 'EqualsToken', 'ExclamationEqualsEqualsToken', 'ExclamationEqualsToken', 'ExportAssignment', 'ExportDeclaration', 'ExportKeyword', 'ExpressionStatement', 'ExternalModuleReference', 'FalseKeyword', 'ForInStatement', 'ForOfStatement', 'ForStatement', 'FunctionDeclaration', 'FunctionExpression', 'GetAccessor', 'GreaterThanEqualsToken', 'GreaterThanToken', 'Identifier', 'IfStatement', 'ImportDeclaration', 'ImportEqualsDeclaration', 'ImportKeyword', 'ImportType', 'InterfaceDeclaration', 'LessThanEqualsToken', 'LessThanToken', 'LiteralType', 'MethodDeclaration', 'ModuleBlock', 'ModuleDeclaration', 'NamedImports', 'NamespaceImport', 'NewExpression', 'NoSubstitutionTemplateLiteral', 'NonNullExpression', 'NullKeyword', 'NumericLiteral', 'ObjectBindingPattern', 'ObjectLiteralExpression', 'OverrideKeyword', 'Parameter', 'ParenthesizedExpression', 'PrivateIdentifier', 'PrivateKeyword', 'PropertyAccessExpression', 'PropertyAssignment', 'PropertyDeclaration', 'ProtectedKeyword', 'PublicKeyword', 'QuestionQuestionEqualsToken', 'QuestionQuestionToken', 'ReadonlyKeyword', 'ReturnStatement', 'SetAccessor', 'ShorthandPropertyAssignment', 'SpreadElement', 'StaticKeyword', 'StringLiteral', 'TemplateExpression', 'ThisKeyword', 'ThrowStatement', 'TrueKeyword', 'TypeAliasDeclaration', 'TypeAssertionExpression', 'TypeOfExpression', 'VariableDeclaration', 'VariableStatement', 'VoidExpression', 'WhileStatement'];
|
|
26
26
|
export type KindName = (typeof KIND_NAMES)[number];
|
|
27
27
|
export type Kinds = Readonly<Record<KindName, number>>;
|
|
28
28
|
/** An open TypeScript project: its user source files and a thenable test over the checker. */
|
|
@@ -31,16 +31,46 @@ export interface TypeScriptProject {
|
|
|
31
31
|
readonly kinds: Kinds;
|
|
32
32
|
/** Program files under the root, outside node_modules, without declaration files. */
|
|
33
33
|
readonly files: readonly TsSourceFile[];
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* D-0120: the browser sources given to openTypeScript (.vue scripts, views/ .js), parsed and
|
|
36
|
+
* typed in a second program of the same tsgo; `path` is the file problems name.
|
|
37
|
+
*/
|
|
38
|
+
readonly browser: readonly {
|
|
39
|
+
readonly file: TsSourceFile;
|
|
40
|
+
readonly path: string;
|
|
41
|
+
}[];
|
|
42
|
+
/** Milliseconds to start tsgo and load the programs (the part a check cannot avoid). */
|
|
35
43
|
readonly loadMs: number;
|
|
36
|
-
/**
|
|
37
|
-
|
|
44
|
+
/**
|
|
45
|
+
* For each expression: is its type a Promise or PromiseLike (or a union with one)? `browser`:
|
|
46
|
+
* the expressions are nodes of the browser program.
|
|
47
|
+
*/
|
|
48
|
+
promises(expressions: readonly TsNode[], browser?: boolean): boolean[];
|
|
49
|
+
/** For each node: has it no type (any, unknown, or none)? An import that resolved to nothing. */
|
|
50
|
+
untyped(nodes: readonly TsNode[], browser?: boolean): boolean[];
|
|
38
51
|
close(): void;
|
|
39
52
|
}
|
|
53
|
+
/** A file outside the program, served to tsgo from memory (check/browser-sources.ts). */
|
|
54
|
+
export interface VirtualSource {
|
|
55
|
+
/** What problems name: root-relative with `/`. */
|
|
56
|
+
readonly path: string;
|
|
57
|
+
/** Absolute file name of the virtual file. */
|
|
58
|
+
readonly fileName: string;
|
|
59
|
+
readonly text: string;
|
|
60
|
+
}
|
|
61
|
+
/** The virtual tsconfig of the browser program; never written to disk. */
|
|
62
|
+
export declare const BROWSER_TSCONFIG = "tsconfig.dokebi-check.json";
|
|
40
63
|
/**
|
|
41
64
|
* Starts the project's TypeScript and loads the program of `<root>/tsconfig.json`. The caller
|
|
42
65
|
* closes it. Throws TYPESCRIPT_UNAVAILABLE (TypeScript missing, older than 7, or its API changed)
|
|
43
66
|
* and TSCONFIG_INVALID (the program cannot be loaded).
|
|
44
67
|
*/
|
|
45
|
-
export declare function openTypeScript(root: string
|
|
68
|
+
export declare function openTypeScript(root: string,
|
|
69
|
+
/**
|
|
70
|
+
* The tsconfig file in `root` (the framework's check:catch gate also reads its other
|
|
71
|
+
* programs); null: no project program, only the browser sources (with standalone options).
|
|
72
|
+
*/
|
|
73
|
+
tsconfig?: string | null,
|
|
74
|
+
/** D-0120: files outside the program read in a second program that extends `tsconfig`. */
|
|
75
|
+
browser?: readonly VirtualSource[]): Promise<TypeScriptProject>;
|
|
46
76
|
export {};
|