@dokebi/cli 0.4.2 → 0.4.26

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.
Files changed (188) hide show
  1. package/AI.md +262 -57
  2. package/README.md +35 -10
  3. package/catalog.json +2519 -257
  4. package/describe.schema.json +748 -0
  5. package/dist/api-app.d.ts +2 -0
  6. package/dist/api-app.js +35 -0
  7. package/dist/app-urls.js +3 -2
  8. package/dist/bin.js +518 -35
  9. package/dist/capability-guide.d.ts +30 -0
  10. package/dist/capability-guide.js +122 -0
  11. package/dist/catalog.d.ts +16 -2
  12. package/dist/catalog.js +19 -7
  13. package/dist/check/asset-rules.d.ts +6 -0
  14. package/dist/check/asset-rules.js +163 -0
  15. package/dist/check/browser-sources.d.ts +20 -0
  16. package/dist/check/browser-sources.js +89 -0
  17. package/dist/check/console-rules.d.ts +6 -0
  18. package/dist/check/console-rules.js +40 -0
  19. package/dist/check/error-rules.d.ts +21 -0
  20. package/dist/check/error-rules.js +212 -0
  21. package/dist/check/form-rules.d.ts +12 -0
  22. package/dist/check/form-rules.js +69 -6
  23. package/dist/check/formatter-rules.d.ts +2 -0
  24. package/dist/check/formatter-rules.js +71 -0
  25. package/dist/check/i18n-rules.d.ts +27 -0
  26. package/dist/check/i18n-rules.js +353 -0
  27. package/dist/check/index.d.ts +15 -1
  28. package/dist/check/index.js +89 -11
  29. package/dist/check/mail-rules.d.ts +4 -0
  30. package/dist/check/mail-rules.js +72 -0
  31. package/dist/check/module-rules.d.ts +13 -0
  32. package/dist/check/module-rules.js +104 -0
  33. package/dist/check/packages.d.ts +8 -0
  34. package/dist/check/packages.js +16 -0
  35. package/dist/check/pool-rules.d.ts +8 -0
  36. package/dist/check/pool-rules.js +164 -0
  37. package/dist/check/problems.d.ts +6 -1
  38. package/dist/check/problems.js +100 -2
  39. package/dist/check/project-rules.d.ts +23 -3
  40. package/dist/check/project-rules.js +265 -23
  41. package/dist/check/seal-rules.d.ts +6 -0
  42. package/dist/check/seal-rules.js +68 -0
  43. package/dist/check/security-rules.d.ts +10 -0
  44. package/dist/check/security-rules.js +450 -0
  45. package/dist/check/shared-state-rules.d.ts +2 -0
  46. package/dist/check/shared-state-rules.js +114 -0
  47. package/dist/check/source-rules.d.ts +26 -3
  48. package/dist/check/source-rules.js +249 -28
  49. package/dist/check/structure-rules.d.ts +23 -0
  50. package/dist/check/structure-rules.js +502 -0
  51. package/dist/check/timer-rules.d.ts +7 -0
  52. package/dist/check/timer-rules.js +543 -0
  53. package/dist/check/typescript.d.ts +35 -5
  54. package/dist/check/typescript.js +163 -17
  55. package/dist/check/vite-rules.d.ts +2 -0
  56. package/dist/check/vite-rules.js +136 -0
  57. package/dist/check/wait-rules.d.ts +16 -0
  58. package/dist/check/wait-rules.js +337 -0
  59. package/dist/commands.d.ts +7 -0
  60. package/dist/commands.js +307 -31
  61. package/dist/config-diff.d.ts +42 -0
  62. package/dist/config-diff.js +289 -0
  63. package/dist/config-edit.d.ts +35 -2
  64. package/dist/config-edit.js +145 -13
  65. package/dist/config-init.d.ts +15 -0
  66. package/dist/config-init.js +61 -0
  67. package/dist/config-paths.d.ts +22 -0
  68. package/dist/config-paths.js +82 -0
  69. package/dist/config-show.d.ts +82 -0
  70. package/dist/config-show.js +563 -0
  71. package/dist/config-values.d.ts +8 -0
  72. package/dist/config-values.js +68 -0
  73. package/dist/configuration-scaffold.d.ts +42 -0
  74. package/dist/configuration-scaffold.js +258 -4
  75. package/dist/configuration.d.ts +63 -3
  76. package/dist/configuration.js +178 -4
  77. package/dist/deployment.d.ts +2 -0
  78. package/dist/deployment.js +111 -0
  79. package/dist/describe-db.d.ts +22 -0
  80. package/dist/describe-db.js +176 -0
  81. package/dist/describe.d.ts +92 -0
  82. package/dist/describe.js +417 -0
  83. package/dist/dev-compose.d.ts +20 -0
  84. package/dist/dev-compose.js +154 -0
  85. package/dist/dev-guard.js +44 -0
  86. package/dist/dev-host.d.ts +13 -0
  87. package/dist/dev-host.js +55 -0
  88. package/dist/dev-infra.d.ts +72 -4
  89. package/dist/dev-infra.js +237 -17
  90. package/dist/dev-watch.d.ts +34 -0
  91. package/dist/dev-watch.js +192 -0
  92. package/dist/diagnostics.d.ts +10 -7
  93. package/dist/diagnostics.js +22 -4
  94. package/dist/drift.d.ts +50 -0
  95. package/dist/drift.js +218 -0
  96. package/dist/env-file.d.ts +25 -1
  97. package/dist/env-file.js +64 -6
  98. package/dist/errors.d.ts +7 -1
  99. package/dist/example-config.d.ts +2 -0
  100. package/dist/example-config.js +3 -0
  101. package/dist/first-admin.d.ts +43 -0
  102. package/dist/first-admin.js +175 -0
  103. package/dist/formatter-ignore.d.ts +37 -0
  104. package/dist/formatter-ignore.js +344 -0
  105. package/dist/generator.d.ts +45 -3
  106. package/dist/generator.js +205 -15
  107. package/dist/guide.d.ts +1 -1
  108. package/dist/guide.js +42 -21
  109. package/dist/install-module.d.ts +20 -2
  110. package/dist/install-module.js +45 -8
  111. package/dist/install-plan.js +5 -3
  112. package/dist/layout.d.ts +12 -1
  113. package/dist/layout.js +19 -6
  114. package/dist/log-writer-control.d.ts +2 -0
  115. package/dist/log-writer-control.js +17 -2
  116. package/dist/log-writer.js +43 -10
  117. package/dist/logging-config.d.ts +8 -3
  118. package/dist/logging-config.js +19 -5
  119. package/dist/manifest.d.ts +29 -1
  120. package/dist/manifest.js +84 -6
  121. package/dist/merge-install-defaults.d.ts +0 -1
  122. package/dist/merge-install-defaults.js +13 -47
  123. package/dist/npm-client.d.ts +40 -4
  124. package/dist/npm-client.js +152 -15
  125. package/dist/npm-paths.js +18 -4
  126. package/dist/openapi.d.ts +27 -0
  127. package/dist/openapi.js +649 -0
  128. package/dist/option-fixes.d.ts +39 -0
  129. package/dist/option-fixes.js +130 -0
  130. package/dist/output.d.ts +1 -1
  131. package/dist/output.js +5 -5
  132. package/dist/plugin-lifecycle.d.ts +34 -2
  133. package/dist/plugin-lifecycle.js +195 -16
  134. package/dist/plugin-options.d.ts +19 -4
  135. package/dist/plugin-options.js +138 -6
  136. package/dist/plugins-command.js +44 -7
  137. package/dist/process-group.d.ts +39 -0
  138. package/dist/process-group.js +132 -0
  139. package/dist/project-guide.d.ts +19 -1
  140. package/dist/project-guide.js +119 -20
  141. package/dist/project-inspector.d.ts +15 -0
  142. package/dist/project-inspector.js +481 -33
  143. package/dist/project-state.d.ts +5 -0
  144. package/dist/project-state.js +8 -0
  145. package/dist/project.d.ts +203 -19
  146. package/dist/project.js +3006 -333
  147. package/dist/recipe-facts.d.ts +127 -0
  148. package/dist/recipe-facts.js +563 -0
  149. package/dist/recipe-login.d.ts +63 -0
  150. package/dist/recipe-login.js +464 -0
  151. package/dist/recipe-mail.d.ts +21 -0
  152. package/dist/recipe-mail.js +214 -0
  153. package/dist/recipe-members.d.ts +35 -0
  154. package/dist/recipe-members.js +178 -0
  155. package/dist/recipe-record.d.ts +82 -7
  156. package/dist/recipe-record.js +226 -22
  157. package/dist/recipe-reset.d.ts +2 -0
  158. package/dist/recipe-reset.js +117 -0
  159. package/dist/recipe-site.d.ts +48 -0
  160. package/dist/recipe-site.js +396 -0
  161. package/dist/recipe-step.d.ts +42 -0
  162. package/dist/recipe-step.js +89 -0
  163. package/dist/recipe.d.ts +56 -20
  164. package/dist/recipe.js +703 -131
  165. package/dist/registry.d.ts +98 -2
  166. package/dist/registry.js +171 -5
  167. package/dist/reloadable.d.ts +35 -0
  168. package/dist/reloadable.js +257 -0
  169. package/dist/server.d.ts +43 -4
  170. package/dist/server.js +601 -52
  171. package/dist/settings.d.ts +48 -0
  172. package/dist/settings.js +168 -0
  173. package/dist/site-auth.d.ts +39 -0
  174. package/dist/site-auth.js +195 -0
  175. package/dist/template-fixes.d.ts +26 -0
  176. package/dist/template-fixes.js +285 -0
  177. package/dist/test-reporter.d.ts +49 -0
  178. package/dist/test-reporter.js +190 -0
  179. package/dist/transaction.d.ts +47 -0
  180. package/dist/transaction.js +241 -7
  181. package/dist/verify-tests.d.ts +98 -0
  182. package/dist/verify-tests.js +449 -0
  183. package/dist/verify.d.ts +103 -3
  184. package/dist/verify.js +272 -21
  185. package/dist/view-choice.d.ts +58 -0
  186. package/dist/view-choice.js +131 -0
  187. package/dist/vite-watch.js +4 -2
  188. package/package.json +8 -6
@@ -0,0 +1,89 @@
1
+ // Browser code for `dokebi check` (D-0120): the `<script>` and `<script setup>` blocks of the
2
+ // project's .vue files and the .js/.mjs files under a views/ folder are not in the tsconfig
3
+ // program (it includes src/**/*.ts), so empty-catch, missing-cause and floating-promise never saw
4
+ // them. Each such file becomes a virtual source next to the original (`<file>.dokebi-check.ts` or
5
+ // `.js`), which typescript.ts serves to tsgo through its virtual file system in a second project:
6
+ // relative imports resolve from the same folder and the checker gives real types. A .vue file
7
+ // keeps its length and its newlines, everything outside the script blocks blanked to spaces, so a
8
+ // line and column of the virtual source are those of the .vue file and the
9
+ // `// dokebi-check: allow …` markers in the script work as in TypeScript.
10
+ import { readFile } from 'node:fs/promises';
11
+ import { join } from 'node:path';
12
+ import { debug } from '../diagnostics.js';
13
+ import { sources } from './form-rules.js';
14
+ /** The suffix of a virtual source; never a real file and never what an import resolves to. */
15
+ export const VIRTUAL_SUFFIX = '.dokebi-check';
16
+ /** Files larger than this are not read (generated bundles), as in form-rules.ts. */
17
+ const MAX_BYTES = 1024 * 1024;
18
+ const BROWSER = /\.(?:vue|js|mjs)$/;
19
+ const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
20
+ const LANG = /\blang\s*=\s*(["']?)([a-z]+)\1/i;
21
+ /**
22
+ * The script of a .vue file as one TypeScript or JavaScript text of the same length: the
23
+ * template, the styles and the tags blanked (newlines kept). Undefined without a script block.
24
+ */
25
+ export function vueScript(text) {
26
+ const out = text.replace(/[^\n]/g, ' ').split('');
27
+ let found = false;
28
+ let typescript = false;
29
+ for (const match of text.matchAll(SCRIPT)) {
30
+ const attributes = match[1] ?? '';
31
+ const body = match[2] ?? '';
32
+ // <script src="…"> loads another file; its body (if any) is not run.
33
+ if (/\bsrc\s*=/.test(attributes) || !body.trim())
34
+ continue;
35
+ const lang = LANG.exec(attributes)?.[2]?.toLowerCase();
36
+ if (lang === 'ts' || lang === 'tsx')
37
+ typescript = true;
38
+ else if (lang !== undefined && lang !== 'js' && lang !== 'jsx')
39
+ continue;
40
+ found = true;
41
+ const start = (match.index ?? 0) + (match[0].indexOf('>') + 1);
42
+ for (let index = 0; index < body.length; index++)
43
+ out[start + index] = body[index];
44
+ }
45
+ return found ? { text: out.join(''), typescript } : undefined;
46
+ }
47
+ /** The virtual source of one file (project-relative `path`), or undefined when there is none. */
48
+ export function browserSource(root, path, text) {
49
+ const absolute = join(root, path);
50
+ if (path.endsWith('.vue')) {
51
+ const script = vueScript(text);
52
+ if (!script)
53
+ return undefined;
54
+ return {
55
+ path,
56
+ fileName: `${absolute}${VIRTUAL_SUFFIX}.${script.typescript ? 'ts' : 'js'}`,
57
+ text: script.text,
58
+ };
59
+ }
60
+ return { path, fileName: `${absolute}${VIRTUAL_SUFFIX}.js`, text };
61
+ }
62
+ /** Is `path` (project-relative) browser code check reads: a .vue file, or .js/.mjs in views/? */
63
+ export function isBrowserPath(path) {
64
+ if (!BROWSER.test(path))
65
+ return false;
66
+ return path.endsWith('.vue') || /(?:^|\/)views\//.test(path);
67
+ }
68
+ /**
69
+ * The browser code under `directory` (default `src/`) of a project: .vue files and the .js/.mjs
70
+ * under views/ (node_modules, dist, .vite, .git and the .tmp scratch folder are not walked).
71
+ */
72
+ export async function browserSources(root, skip = () => false, directory = 'src') {
73
+ const found = [];
74
+ let large = 0;
75
+ for (const path of await sources(root, directory, BROWSER)) {
76
+ if (!isBrowserPath(path) || skip(path))
77
+ continue;
78
+ const text = await readFile(join(root, path), 'utf8');
79
+ if (text.length > MAX_BYTES) {
80
+ large++;
81
+ continue;
82
+ }
83
+ const source = browserSource(root, path, text);
84
+ if (source)
85
+ found.push(source);
86
+ }
87
+ debug('check: browser sources', { files: found.length, skippedLarge: large });
88
+ return found;
89
+ }
@@ -0,0 +1,6 @@
1
+ import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
2
+ export type Report = (node: TsNode, message: string, fix: string) => void;
3
+ export declare const CONSOLE_FIX = "log through the Dokebi logger instead: context.logger.log('info', 'Fixed message', { code: 'MY_CODE', \u2026 }) in a plugin or module hook, app.logger in src/main.ts (levels, redaction and every target apply); for a command line tool that prints results, write // dokebi-check: allow console-output <reason> before its first statement";
4
+ /** `console.x`, `globalThis.console.x`, `process.stdout.write`, `process.stderr.write`. */
5
+ export declare function consoleTarget(file: TsSourceFile, callee: TsNode): string | undefined;
6
+ export declare function consoleRules(file: TsSourceFile, kinds: Kinds, report: Report): void;
@@ -0,0 +1,40 @@
1
+ // The console rule of `dokebi check` (D-0151 §13), over the TypeScript AST:
2
+ // console-output (warning) server code under src/ (not views/, not tests/) that writes records
3
+ // past the app logger: console.log/info/warn/error/debug/trace and
4
+ // process.stdout.write / process.stderr.write. Those lines skip the logger's
5
+ // level, redaction (password, token, cookie …), targets (rotated files) and
6
+ // the JSON line `dokebi logs` and log collectors read. Use the logger the
7
+ // plugin context or the app hands out instead.
8
+ // A `// dokebi-check: allow console-output <reason>` comment on or above the line keeps a
9
+ // deliberate one; the same comment before the first statement of a file covers the whole file
10
+ // (a command line tool that prints its results). The framework's own sources pass the same rule
11
+ // as a gate (scripts/testing/check-console.mjs), where it is an error.
12
+ import { field } from './typescript.js';
13
+ const CONSOLE_METHODS = new Set(['log', 'info', 'warn', 'error', 'debug', 'trace']);
14
+ export const CONSOLE_FIX = "log through the Dokebi logger instead: context.logger.log('info', 'Fixed message', { code: 'MY_CODE', … }) in a plugin or module hook, app.logger in src/main.ts (levels, redaction and every target apply); for a command line tool that prints results, write // dokebi-check: allow console-output <reason> before its first statement";
15
+ /** `console.x`, `globalThis.console.x`, `process.stdout.write`, `process.stderr.write`. */
16
+ export function consoleTarget(file, callee) {
17
+ const text = file.text.slice(callee.getStart(), callee.end).replace(/\s+/g, '');
18
+ const match = /^(?:globalThis\.)?console\.([a-z]+)$/.exec(text);
19
+ if (match && CONSOLE_METHODS.has(match[1]))
20
+ return text;
21
+ return /^(?:globalThis\.)?process\.(?:stdout|stderr)\.write$/.test(text) ? text : undefined;
22
+ }
23
+ export function consoleRules(file, kinds, report) {
24
+ // Most files print nothing: skip the walk.
25
+ if (!/\bconsole\s*\.|\bprocess\s*\.\s*std(?:out|err)\b/.test(file.text))
26
+ return;
27
+ const visit = (node) => {
28
+ if (node.kind === kinds.CallExpression) {
29
+ const callee = field(node, 'expression');
30
+ if (callee?.kind === kinds.PropertyAccessExpression) {
31
+ const target = consoleTarget(file, callee);
32
+ if (target)
33
+ report(node, `${target}() writes past the app logger: no level, no redaction of secrets, not in the log files or the JSON lines dokebi logs reads (D-0151)`, CONSOLE_FIX);
34
+ }
35
+ }
36
+ node.forEachChild(visit);
37
+ return undefined;
38
+ };
39
+ file.forEachChild(visit);
40
+ }
@@ -0,0 +1,21 @@
1
+ import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
2
+ /** Where the app's error codes are declared (D-0077 D). */
3
+ export declare const ERRORS_FILE = "src/shared/errors.ts";
4
+ /** Codes of the framework's own answers: an app may throw or send them without listing them. */
5
+ export declare const FRAMEWORK_CODES: ReadonlySet<string>;
6
+ /** The declared list: the codes and the name the file exports it under. */
7
+ export interface ErrorList {
8
+ readonly codes: ReadonlySet<string>;
9
+ /** `errors` for `export const errors = defineErrors(…)`, `default` for a default export. */
10
+ readonly exportName: string;
11
+ }
12
+ export type Report = (node: TsNode, message: string, fix: string) => void;
13
+ /**
14
+ * The list declared in src/shared/errors.ts: `export const errors = defineErrors({ … })`,
15
+ * `export default defineErrors({ … })`, or a top-level const exported with `export { errors }`.
16
+ * undefined when the file exports none, or its object is not a plain literal (spread, computed).
17
+ */
18
+ export declare function readErrorList(file: TsSourceFile, kinds: Kinds): ErrorList | undefined;
19
+ /** Whether the rule reads this project file. */
20
+ export declare function isAppCode(path: string): boolean;
21
+ export declare function errorCodeRules(file: TsSourceFile, path: string, kinds: Kinds, list: ErrorList, report: Report): void;
@@ -0,0 +1,212 @@
1
+ // The error code rule of `dokebi check` (D-0077 D), on only in a project whose
2
+ // `src/shared/errors.ts` exports a `defineErrors({ … })` result written as an object literal:
3
+ // unknown-error-code (warning) app code (src/apps, src/shared/modules, src/shared/services) that writes an error
4
+ // code as a string instead of `errors.CODE()`: `new DokebiError('CODE', …)`,
5
+ // `Object.assign(new Error(…), { code: 'CODE' })`, `throw { code: 'CODE' }`
6
+ // and a `.send({ error: 'CODE' })` body. A code missing from the list is
7
+ // reported as unknown, a listed one as written by hand (its status,
8
+ // message and hint live in the list). Codes dokebi-web and the auth and
9
+ // csrf plugins answer with themselves (VALIDATION_FAILED, NOT_FOUND,
10
+ // UNAUTHENTICATED, FORBIDDEN, …) are fine.
11
+ // The list is read from the file's AST, never by importing it: a spread or a computed key makes
12
+ // the list unknowable and turns the rule off for the project.
13
+ import { posix } from 'node:path';
14
+ import { debug } from '../diagnostics.js';
15
+ import { field, nodes, property, } from './typescript.js';
16
+ /** Where the app's error codes are declared (D-0077 D). */
17
+ export const ERRORS_FILE = 'src/shared/errors.ts';
18
+ /** A stable error code (D-0063). */
19
+ const CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
20
+ /** Codes of the framework's own answers: an app may throw or send them without listing them. */
21
+ export const FRAMEWORK_CODES = new Set([
22
+ 'VALIDATION_FAILED',
23
+ 'NOT_FOUND',
24
+ 'INVALID_REQUEST',
25
+ 'INTERNAL_SERVER_ERROR',
26
+ 'UNAUTHENTICATED',
27
+ 'FORBIDDEN',
28
+ 'CSRF_FAILED',
29
+ ]);
30
+ /** Folders of app code (D-0031, D-0032): the rule reads only these. */
31
+ const APP_CODE = /^src\/(apps|shared\/(modules|services))\//;
32
+ const text = (node) => node === undefined ? undefined : String(property(node, 'text'));
33
+ /** True when `node` is a `defineErrors(…)` or `x.defineErrors(…)` call. */
34
+ function defineCall(node, kinds) {
35
+ if (node?.kind !== kinds.CallExpression)
36
+ return false;
37
+ const callee = field(node, 'expression');
38
+ if (callee?.kind === kinds.Identifier)
39
+ return text(callee) === 'defineErrors';
40
+ return (callee?.kind === kinds.PropertyAccessExpression &&
41
+ text(field(callee, 'name')) === 'defineErrors');
42
+ }
43
+ /** The codes of the object literal given to defineErrors, or undefined when not knowable. */
44
+ function literalCodes(call, kinds) {
45
+ const argument = nodes(call, 'arguments')[0];
46
+ if (argument?.kind !== kinds.ObjectLiteralExpression)
47
+ return undefined;
48
+ const codes = new Set();
49
+ for (const item of nodes(argument, 'properties')) {
50
+ const name = field(item, 'name');
51
+ if (item.kind !== kinds.PropertyAssignment ||
52
+ !name ||
53
+ (name.kind !== kinds.Identifier && name.kind !== kinds.StringLiteral))
54
+ return undefined;
55
+ codes.add(text(name));
56
+ }
57
+ return codes;
58
+ }
59
+ /**
60
+ * The list declared in src/shared/errors.ts: `export const errors = defineErrors({ … })`,
61
+ * `export default defineErrors({ … })`, or a top-level const exported with `export { errors }`.
62
+ * undefined when the file exports none, or its object is not a plain literal (spread, computed).
63
+ */
64
+ export function readErrorList(file, kinds) {
65
+ const local = new Map();
66
+ const exported = [];
67
+ const named = [];
68
+ for (const statement of nodes(file, 'statements')) {
69
+ if (statement.kind === kinds.VariableStatement) {
70
+ const isExported = nodes(statement, 'modifiers').some((modifier) => modifier.kind === kinds.ExportKeyword);
71
+ const list = field(statement, 'declarationList');
72
+ for (const declaration of list ? nodes(list, 'declarations') : []) {
73
+ const name = field(declaration, 'name');
74
+ const initializer = field(declaration, 'initializer');
75
+ if (name?.kind !== kinds.Identifier || !defineCall(initializer, kinds))
76
+ continue;
77
+ if (isExported)
78
+ exported.push({ name: text(name), call: initializer });
79
+ else
80
+ local.set(text(name), initializer);
81
+ }
82
+ }
83
+ else if (statement.kind === kinds.ExportAssignment) {
84
+ const expression = field(statement, 'expression');
85
+ if (property(statement, 'isExportEquals') !== true && defineCall(expression, kinds))
86
+ exported.push({ name: 'default', call: expression });
87
+ }
88
+ else if (statement.kind === kinds.ExportDeclaration && !field(statement, 'moduleSpecifier')) {
89
+ const clause = field(statement, 'exportClause');
90
+ for (const element of clause ? nodes(clause, 'elements') : [])
91
+ named.push({
92
+ local: text(field(element, 'propertyName') ?? field(element, 'name')) ?? '',
93
+ name: text(field(element, 'name')) ?? '',
94
+ });
95
+ }
96
+ }
97
+ for (const item of named) {
98
+ const call = local.get(item.local);
99
+ if (call)
100
+ exported.push({ name: item.name, call });
101
+ }
102
+ if (exported.length === 0)
103
+ return undefined;
104
+ const codes = new Set();
105
+ for (const item of exported) {
106
+ const found = literalCodes(item.call, kinds);
107
+ if (!found) {
108
+ debug('check: error list not readable', { file: ERRORS_FILE, export: item.name });
109
+ return undefined;
110
+ }
111
+ for (const code of found)
112
+ codes.add(code);
113
+ }
114
+ const first = exported[0];
115
+ debug('check: error list read', { file: ERRORS_FILE, codes: codes.size, export: first.name });
116
+ return { codes, exportName: first.name };
117
+ }
118
+ /** Whether the rule reads this project file. */
119
+ export function isAppCode(path) {
120
+ return APP_CODE.test(path);
121
+ }
122
+ export function errorCodeRules(file, path, kinds, list, report) {
123
+ const source = file.text;
124
+ const snippet = (node) => {
125
+ const value = source.slice(node.getStart(), node.end).replace(/\s+/g, ' ');
126
+ return value.length > 60 ? `${value.slice(0, 59)}…` : value;
127
+ };
128
+ const literal = (node) => node && (node.kind === kinds.StringLiteral || node.kind === kinds.NoSubstitutionTemplateLiteral)
129
+ ? String(property(node, 'text'))
130
+ : undefined;
131
+ /** The string value of property `key` in an object literal. */
132
+ const propertyValue = (object, key) => {
133
+ if (object?.kind !== kinds.ObjectLiteralExpression)
134
+ return undefined;
135
+ for (const item of nodes(object, 'properties')) {
136
+ if (item.kind !== kinds.PropertyAssignment)
137
+ continue;
138
+ const name = field(item, 'name');
139
+ if (name &&
140
+ (name.kind === kinds.Identifier || name.kind === kinds.StringLiteral) &&
141
+ text(name) === key) {
142
+ const value = field(item, 'initializer');
143
+ return literal(value) === undefined ? undefined : value;
144
+ }
145
+ }
146
+ return undefined;
147
+ };
148
+ let importPath = posix.relative(posix.dirname(path), ERRORS_FILE);
149
+ if (!importPath.startsWith('.'))
150
+ importPath = `./${importPath}`;
151
+ const binding = list.exportName === 'default'
152
+ ? `import errors from '${importPath}'`
153
+ : `import { ${list.exportName} } from '${importPath}'`;
154
+ const factory = list.exportName === 'default' ? 'errors' : list.exportName;
155
+ const declared = [...list.codes];
156
+ const some = declared.slice(0, 5).join(', ') + (declared.length > 5 ? ', …' : '');
157
+ /** Reports the code at `node` when it is not a framework code. */
158
+ const judge = (node, at) => {
159
+ const code = literal(node);
160
+ if (code === undefined || !CODE.test(code) || FRAMEWORK_CODES.has(code))
161
+ return;
162
+ if (list.codes.has(code))
163
+ report(at, `${snippet(at)} writes the error code ${code} by hand; the list in ${ERRORS_FILE} declares it with its status, message and hint`, `throw ${factory}.${code}() (${binding}); pass { data } for values the client needs`);
164
+ else
165
+ report(at, `${snippet(at)} uses the error code ${code}, which ${ERRORS_FILE} does not declare: clients and tools only know the codes of the list`, `add ${code}: { status, message } to defineErrors in ${ERRORS_FILE} and throw ${factory}.${code}() (${binding}), or use a declared code (${some}); refused fields use validationError() from @dokebi/web. A deliberate case takes // dokebi-check: allow unknown-error-code <reason>`);
166
+ };
167
+ const isObjectAssign = (call) => {
168
+ const callee = field(call, 'expression');
169
+ return (callee?.kind === kinds.PropertyAccessExpression &&
170
+ text(field(callee, 'name')) === 'assign' &&
171
+ field(callee, 'expression')?.kind === kinds.Identifier &&
172
+ text(field(callee, 'expression')) === 'Object');
173
+ };
174
+ const visit = (node) => {
175
+ if (node.kind === kinds.NewExpression) {
176
+ const callee = field(node, 'expression');
177
+ const name = callee?.kind === kinds.Identifier
178
+ ? text(callee)
179
+ : callee?.kind === kinds.PropertyAccessExpression
180
+ ? text(field(callee, 'name'))
181
+ : undefined;
182
+ const first = nodes(node, 'arguments')[0];
183
+ if (name === 'DokebiError' && first)
184
+ judge(first, node);
185
+ }
186
+ else if (node.kind === kinds.CallExpression) {
187
+ const args = nodes(node, 'arguments');
188
+ const callee = field(node, 'expression');
189
+ if (isObjectAssign(node) && args[0]?.kind === kinds.NewExpression) {
190
+ for (const item of args.slice(1)) {
191
+ const value = propertyValue(item, 'code');
192
+ if (value)
193
+ judge(value, node);
194
+ }
195
+ }
196
+ else if (callee?.kind === kinds.PropertyAccessExpression &&
197
+ text(field(callee, 'name')) === 'send') {
198
+ const value = propertyValue(args[0], 'error');
199
+ if (value)
200
+ judge(value, node);
201
+ }
202
+ }
203
+ else if (node.kind === kinds.ThrowStatement) {
204
+ const value = propertyValue(field(node, 'expression'), 'code');
205
+ if (value)
206
+ judge(value, node);
207
+ }
208
+ node.forEachChild(visit);
209
+ return undefined;
210
+ };
211
+ file.forEachChild(visit);
212
+ }
@@ -6,8 +6,20 @@ import type { Problem } from './problems.js';
6
6
  export declare function maskCode(text: string): string;
7
7
  /** The problems of one file's text (the path is project-relative, with `/`). */
8
8
  export declare function inertiaFormProblems(path: string, text: string): Problem[];
9
+ /**
10
+ * Every source file (or file matching `pattern`) under `directory` (`.`: the whole root),
11
+ * root-relative with `/`.
12
+ */
13
+ export declare function sources(root: string, directory?: string, pattern?: RegExp): Promise<string[]>;
9
14
  /** The form rule over the project's `src/` (D-0074). */
10
15
  export declare function formProblems(root: string, skip: (path: string) => boolean): Promise<{
11
16
  problems: Problem[];
12
17
  files: number;
13
18
  }>;
19
+ /** The CSRF problems of one EJS file's text (the path is project-relative, with `/`). */
20
+ export declare function csrfFormProblems(path: string, text: string): Problem[];
21
+ /** The CSRF form rule over the EJS views under `src/` (only with @dokebi/csrf installed). */
22
+ export declare function csrfFormsProblems(root: string): Promise<{
23
+ problems: Problem[];
24
+ files: number;
25
+ }>;
@@ -16,7 +16,8 @@ import { debug } from '../diagnostics.js';
16
16
  import { readMarkers } from './source-rules.js';
17
17
  const RULE = 'inertia-form-only';
18
18
  const SOURCE = /\.(?:vue|svelte|ts|tsx|js|jsx|mjs)$/;
19
- const SKIP_DIRECTORIES = new Set(['node_modules', 'dist', '.vite', '.git']);
19
+ const EJS = /\.ejs$/;
20
+ const SKIP_DIRECTORIES = new Set(['node_modules', 'dist', '.vite', '.git', '.tmp']);
20
21
  /** Files larger than this are not read (generated bundles, data). */
21
22
  const MAX_BYTES = 1024 * 1024;
22
23
  const SUBMIT = new Set(['post', 'put', 'patch', 'delete']);
@@ -182,8 +183,11 @@ export function inertiaFormProblems(path, text) {
182
183
  }
183
184
  return problems;
184
185
  }
185
- /** Every source file under `src/`, project-relative with `/`. */
186
- async function sources(root, directory = 'src') {
186
+ /**
187
+ * Every source file (or file matching `pattern`) under `directory` (`.`: the whole root),
188
+ * root-relative with `/`.
189
+ */
190
+ export async function sources(root, directory = 'src', pattern = SOURCE) {
187
191
  let entries;
188
192
  try {
189
193
  entries = await readdir(join(root, directory), { withFileTypes: true });
@@ -196,12 +200,12 @@ async function sources(root, directory = 'src') {
196
200
  }
197
201
  const files = [];
198
202
  for (const entry of entries) {
199
- const path = `${directory}/${entry.name}`;
203
+ const path = directory === '.' ? entry.name : `${directory}/${entry.name}`;
200
204
  if (entry.isDirectory()) {
201
205
  if (!SKIP_DIRECTORIES.has(entry.name))
202
- files.push(...(await sources(root, path)));
206
+ files.push(...(await sources(root, path, pattern)));
203
207
  }
204
- else if (entry.isFile() && SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts'))
208
+ else if (entry.isFile() && pattern.test(entry.name) && !entry.name.endsWith('.d.ts'))
205
209
  files.push(path);
206
210
  }
207
211
  return files;
@@ -222,3 +226,62 @@ export async function formProblems(root, skip) {
222
226
  debug('check: inertia forms', { files, problems: problems.length });
223
227
  return { problems, files };
224
228
  }
229
+ // form-without-csrf (D-0076 §6, a warning, only with @dokebi/csrf installed): an EJS
230
+ // `<form method="post">` (any case, quoted or not) whose body up to `</form>` has neither
231
+ // `csrf.field` (`<%- data.csrf.field %>`, also `csrf?.field`) nor a field named `_csrf`, and whose action carries no
232
+ // `_csrf=` (a multipart form). @dokebi/csrf refuses such a post with 403 CSRF_FAILED. EJS is text:
233
+ // a form written by script, or opened and closed in different includes, is not seen as one.
234
+ const CSRF_RULE = 'form-without-csrf';
235
+ const FORM = /<form\b[^>]*>/gi;
236
+ const POST = /\bmethod\s*=\s*(["']?)post\1(?=[\s>/]|$)/i;
237
+ // `csrf?.field` too: a layout also drawn without csrf data (an error page) reads it optionally (M51).
238
+ const HAS_TOKEN = /csrf\??\.field|name\s*=\s*["']?_csrf\b|[?&]_csrf=/;
239
+ /** The CSRF problems of one EJS file's text (the path is project-relative, with `/`). */
240
+ export function csrfFormProblems(path, text) {
241
+ const problems = [];
242
+ const markers = readMarkers(text).filter((marker) => marker.rule === CSRF_RULE);
243
+ const lower = text.toLowerCase();
244
+ const lineAt = (position) => {
245
+ let line = 1;
246
+ for (let index = 0; index < position; index++)
247
+ if (text.charCodeAt(index) === 10)
248
+ line++;
249
+ return line;
250
+ };
251
+ for (const match of text.matchAll(FORM)) {
252
+ if (!POST.test(match[0]))
253
+ continue;
254
+ const start = match.index ?? 0;
255
+ const close = lower.indexOf('</form', start + match[0].length);
256
+ if (HAS_TOKEN.test(text.slice(start, close === -1 ? text.length : close)))
257
+ continue;
258
+ const line = lineAt(start);
259
+ // An EJS or HTML comment marker ends with `%>` or `-->`; the words before it are the reason.
260
+ const marker = markers.find((item) => item.line === line || item.line === line - 1);
261
+ if (marker?.reason.replace(/\s*[-_%]*>\s*$/, '').trim())
262
+ continue;
263
+ problems.push({
264
+ rule: CSRF_RULE,
265
+ severity: 'warning',
266
+ file: path,
267
+ line,
268
+ message: `A <form method="post"> without the CSRF token: @dokebi/csrf refuses its post with 403 CSRF_FAILED${marker ? ' (the dokebi-check marker has no reason, so it does not count)' : ''}`,
269
+ fix: 'write <%- data.csrf.field %> inside the form (a multipart form: action="…?_csrf=<%= data.csrf.token %>"); never exempt the route with csrf: false to make a form work. A deliberate case takes <%# dokebi-check: allow form-without-csrf <reason> %> on the line above',
270
+ });
271
+ }
272
+ return problems;
273
+ }
274
+ /** The CSRF form rule over the EJS views under `src/` (only with @dokebi/csrf installed). */
275
+ export async function csrfFormsProblems(root) {
276
+ const problems = [];
277
+ let files = 0;
278
+ for (const path of await sources(root, 'src', EJS)) {
279
+ const text = await readFile(join(root, path), 'utf8');
280
+ if (text.length > MAX_BYTES)
281
+ continue;
282
+ files++;
283
+ problems.push(...csrfFormProblems(path, text));
284
+ }
285
+ debug('check: EJS forms and CSRF', { files, problems: problems.length });
286
+ return { problems, files };
287
+ }
@@ -0,0 +1,2 @@
1
+ import type { Problem } from './problems.js';
2
+ export declare function formatterProblems(root: string): Promise<Problem[]>;
@@ -0,0 +1,71 @@
1
+ // The formatter rule of `dokebi check` (D-0145, a warning):
2
+ // formatter-migrations a code formatter of the project would format the migrations folder
3
+ // (`<dokebi-migrate directory>/migrations`): prettier without the folder
4
+ // in .prettierignore, biome without `!<folder>` in files.includes (or the
5
+ // folder in files.ignore of biome 1), ESLint with formatting rules
6
+ // (eslint-plugin-prettier, @stylistic) whose config does not name it.
7
+ // A reformatted applied migration is only a warning of @dokebi/migrate (MIGRATE_REFORMATTED, its
8
+ // token checksum is unchanged), but its bytes should not change at all: the history records them.
9
+ // Only for a project whose migrations folder exists.
10
+ // formatter-generated the same formatters would format src/dokebi.generated.ts, which the CLI
11
+ // writes and compares with its record: a reformatted copy is
12
+ // MODIFIED_REGISTRATION, and doctor fails (R1 measurement: a builder's
13
+ // biome over src/ rewrote 32 lines of it).
14
+ import { stat } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { debug, isMissing } from '../diagnostics.js';
17
+ import { CliError } from '../errors.js';
18
+ import { formatterFix, formatterUses, GENERATED_FILE, migrationsFolder, } from '../formatter-ignore.js';
19
+ const RULE = 'formatter-migrations';
20
+ /** A file or folder that exists under the project. */
21
+ async function present(root, path) {
22
+ try {
23
+ await stat(join(root, path));
24
+ return true;
25
+ }
26
+ catch (error) {
27
+ if (isMissing(error))
28
+ return false;
29
+ throw new CliError('READ_FAILED', `Cannot read ${path}`, { cause: error });
30
+ }
31
+ }
32
+ export async function formatterProblems(root) {
33
+ const problems = [];
34
+ if (await present(root, GENERATED_FILE))
35
+ for (const use of await formatterUses(root, GENERATED_FILE)) {
36
+ if (use.covered)
37
+ continue;
38
+ problems.push({
39
+ rule: 'formatter-generated',
40
+ severity: 'warning',
41
+ file: use.config,
42
+ line: 0,
43
+ message: `${use.tool} would format ${GENERATED_FILE}: the CLI writes it and refuses a project whose copy differs (MODIFIED_REGISTRATION)`,
44
+ fix: formatterFix(use, GENERATED_FILE),
45
+ });
46
+ }
47
+ const folder = await migrationsFolder(root);
48
+ try {
49
+ if (!(await stat(join(root, folder))).isDirectory())
50
+ return problems;
51
+ }
52
+ catch (error) {
53
+ if (isMissing(error))
54
+ return problems;
55
+ throw new CliError('READ_FAILED', `Cannot read ${folder}`, { cause: error });
56
+ }
57
+ for (const use of await formatterUses(root, folder)) {
58
+ if (use.covered)
59
+ continue;
60
+ problems.push({
61
+ rule: RULE,
62
+ severity: 'warning',
63
+ file: use.config,
64
+ line: 0,
65
+ message: `${use.tool} would format ${folder}/: an applied migration must keep its contents (@dokebi/migrate records them)`,
66
+ fix: formatterFix(use, folder),
67
+ });
68
+ }
69
+ debug('check: formatter rule done', { folder, problems: problems.length });
70
+ return problems;
71
+ }
@@ -0,0 +1,27 @@
1
+ import type { Problem } from './problems.js';
2
+ /** One namespace file of one layer and locale. */
3
+ interface CatalogFile {
4
+ readonly path: string;
5
+ readonly text: string;
6
+ readonly keys: ReadonlySet<string>;
7
+ /** Its `$overrides`, as full keys (`ns.path`). */
8
+ readonly overrides: ReadonlySet<string>;
9
+ }
10
+ /** One layer: its files by locale and namespace. */
11
+ interface Layer {
12
+ readonly kind: 'app' | 'module' | 'shared';
13
+ readonly name: string;
14
+ readonly folder: string;
15
+ readonly files: ReadonlyMap<string, ReadonlyMap<string, CatalogFile>>;
16
+ }
17
+ /** i18n-layer-override: a key of one language in two layers, unless the overriding file lists it. */
18
+ export declare function overrideProblems(shared: Layer | undefined, modules: readonly Layer[], apps: readonly Layer[]): Problem[];
19
+ /** i18n-layer-missing: keys a layer gives in one language and lacks in another of the project. */
20
+ export declare function layerMissingProblems(layers: readonly Layer[], locales: readonly string[]): Problem[];
21
+ /** The problems of one file's text against the catalogs (the path is project-relative). */
22
+ export declare function missingKeyProblems(path: string, text: string, catalogs: ReadonlyMap<string, ReadonlySet<string>>,
23
+ /** dokebi-i18n defaultLocale: the end of every fallback chain. */
24
+ defaultLocale?: string): Problem[];
25
+ /** The translation rules over the catalogs and the files under `src/` (only with @dokebi/i18n). */
26
+ export declare function missingKeysProblems(root: string, skip: (path: string) => boolean): Promise<Problem[]>;
27
+ export {};