@kvman/testkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/bin/bin-failure.d.ts +16 -0
- package/dist/bin/bin-failure.d.ts.map +1 -0
- package/dist/bin/bin-failure.js +28 -0
- package/dist/bin/bin-failure.js.map +1 -0
- package/dist/bin/program-command.d.ts +7 -0
- package/dist/bin/program-command.d.ts.map +1 -0
- package/dist/bin/program-command.js +8 -0
- package/dist/bin/program-command.js.map +1 -0
- package/dist/bin/run-program.d.ts +10 -0
- package/dist/bin/run-program.d.ts.map +1 -0
- package/dist/bin/run-program.js +30 -0
- package/dist/bin/run-program.js.map +1 -0
- package/dist/check/check-extension.d.ts +4 -0
- package/dist/check/check-extension.d.ts.map +1 -0
- package/dist/check/check-extension.js +78 -0
- package/dist/check/check-extension.js.map +1 -0
- package/dist/check/check-output.d.ts +4 -0
- package/dist/check/check-output.d.ts.map +1 -0
- package/dist/check/check-output.js +13 -0
- package/dist/check/check-output.js.map +1 -0
- package/dist/check/docs-findings.d.ts +8 -0
- package/dist/check/docs-findings.d.ts.map +1 -0
- package/dist/check/docs-findings.js +65 -0
- package/dist/check/docs-findings.js.map +1 -0
- package/dist/check/field-descriptions.d.ts +8 -0
- package/dist/check/field-descriptions.d.ts.map +1 -0
- package/dist/check/field-descriptions.js +19 -0
- package/dist/check/field-descriptions.js.map +1 -0
- package/dist/check/finding.d.ts +11 -0
- package/dist/check/finding.d.ts.map +1 -0
- package/dist/check/finding.js +2 -0
- package/dist/check/finding.js.map +1 -0
- package/dist/check/locale-keys.d.ts +7 -0
- package/dist/check/locale-keys.d.ts.map +1 -0
- package/dist/check/locale-keys.js +40 -0
- package/dist/check/locale-keys.js.map +1 -0
- package/dist/check/timer-scan.d.ts +6 -0
- package/dist/check/timer-scan.d.ts.map +1 -0
- package/dist/check/timer-scan.js +84 -0
- package/dist/check/timer-scan.js.map +1 -0
- package/dist/check/ui-keys.d.ts +3 -0
- package/dist/check/ui-keys.d.ts.map +1 -0
- package/dist/check/ui-keys.js +58 -0
- package/dist/check/ui-keys.js.map +1 -0
- package/dist/check-bin.d.ts +3 -0
- package/dist/check-bin.d.ts.map +1 -0
- package/dist/check-bin.js +17 -0
- package/dist/check-bin.js.map +1 -0
- package/dist/docs/built-in-guides.d.ts +12 -0
- package/dist/docs/built-in-guides.d.ts.map +1 -0
- package/dist/docs/built-in-guides.js +20 -0
- package/dist/docs/built-in-guides.js.map +1 -0
- package/dist/docs/docs-bin.d.ts +3 -0
- package/dist/docs/docs-bin.d.ts.map +1 -0
- package/dist/docs/docs-bin.js +98 -0
- package/dist/docs/docs-bin.js.map +1 -0
- package/dist/docs/docs-get.d.ts +11 -0
- package/dist/docs/docs-get.d.ts.map +1 -0
- package/dist/docs/docs-get.js +54 -0
- package/dist/docs/docs-get.js.map +1 -0
- package/dist/docs/docs-list.d.ts +22 -0
- package/dist/docs/docs-list.d.ts.map +1 -0
- package/dist/docs/docs-list.js +51 -0
- package/dist/docs/docs-list.js.map +1 -0
- package/dist/fake-clock.d.ts +16 -0
- package/dist/fake-clock.d.ts.map +1 -0
- package/dist/fake-clock.js +47 -0
- package/dist/fake-clock.js.map +1 -0
- package/dist/fake-openai.d.ts +55 -0
- package/dist/fake-openai.d.ts.map +1 -0
- package/dist/fake-openai.js +115 -0
- package/dist/fake-openai.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -0
- package/dist/new/new-bin.d.ts +3 -0
- package/dist/new/new-bin.d.ts.map +1 -0
- package/dist/new/new-bin.js +60 -0
- package/dist/new/new-bin.js.map +1 -0
- package/dist/new/new-project.d.ts +11 -0
- package/dist/new/new-project.d.ts.map +1 -0
- package/dist/new/new-project.js +36 -0
- package/dist/new/new-project.js.map +1 -0
- package/dist/new/scaffold-files.d.ts +7 -0
- package/dist/new/scaffold-files.d.ts.map +1 -0
- package/dist/new/scaffold-files.js +100 -0
- package/dist/new/scaffold-files.js.map +1 -0
- package/dist/new/scaffold-versions.d.ts +10 -0
- package/dist/new/scaffold-versions.d.ts.map +1 -0
- package/dist/new/scaffold-versions.js +12 -0
- package/dist/new/scaffold-versions.js.map +1 -0
- package/dist/preset/preset-bin.d.ts +3 -0
- package/dist/preset/preset-bin.d.ts.map +1 -0
- package/dist/preset/preset-bin.js +91 -0
- package/dist/preset/preset-bin.js.map +1 -0
- package/dist/preset/preset-check.d.ts +15 -0
- package/dist/preset/preset-check.d.ts.map +1 -0
- package/dist/preset/preset-check.js +113 -0
- package/dist/preset/preset-check.js.map +1 -0
- package/dist/preset/preset-new.d.ts +5 -0
- package/dist/preset/preset-new.d.ts.map +1 -0
- package/dist/preset/preset-new.js +16 -0
- package/dist/preset/preset-new.js.map +1 -0
- package/dist/preset/preset-reach.d.ts +18 -0
- package/dist/preset/preset-reach.d.ts.map +1 -0
- package/dist/preset/preset-reach.js +33 -0
- package/dist/preset/preset-reach.js.map +1 -0
- package/dist/preset/project-manifest.d.ts +5 -0
- package/dist/preset/project-manifest.d.ts.map +1 -0
- package/dist/preset/project-manifest.js +30 -0
- package/dist/preset/project-manifest.js.map +1 -0
- package/dist/preview/preview-bin.d.ts +3 -0
- package/dist/preview/preview-bin.d.ts.map +1 -0
- package/dist/preview/preview-bin.js +63 -0
- package/dist/preview/preview-bin.js.map +1 -0
- package/dist/preview/preview-home.d.ts +5 -0
- package/dist/preview/preview-home.d.ts.map +1 -0
- package/dist/preview/preview-home.js +64 -0
- package/dist/preview/preview-home.js.map +1 -0
- package/dist/preview/preview-manifest.d.ts +9 -0
- package/dist/preview/preview-manifest.d.ts.map +1 -0
- package/dist/preview/preview-manifest.js +32 -0
- package/dist/preview/preview-manifest.js.map +1 -0
- package/dist/preview/preview-port.d.ts +5 -0
- package/dist/preview/preview-port.d.ts.map +1 -0
- package/dist/preview/preview-port.js +22 -0
- package/dist/preview/preview-port.js.map +1 -0
- package/dist/preview/preview-preset.d.ts +14 -0
- package/dist/preview/preview-preset.d.ts.map +1 -0
- package/dist/preview/preview-preset.js +13 -0
- package/dist/preview/preview-preset.js.map +1 -0
- package/dist/preview/preview-run.d.ts +14 -0
- package/dist/preview/preview-run.d.ts.map +1 -0
- package/dist/preview/preview-run.js +241 -0
- package/dist/preview/preview-run.js.map +1 -0
- package/dist/preview/process-tree.d.ts +40 -0
- package/dist/preview/process-tree.d.ts.map +1 -0
- package/dist/preview/process-tree.js +92 -0
- package/dist/preview/process-tree.js.map +1 -0
- package/dist/running/call-query.d.ts +5 -0
- package/dist/running/call-query.d.ts.map +1 -0
- package/dist/running/call-query.js +34 -0
- package/dist/running/call-query.js.map +1 -0
- package/dist/running/running-kvman.d.ts +14 -0
- package/dist/running/running-kvman.d.ts.map +1 -0
- package/dist/running/running-kvman.js +80 -0
- package/dist/running/running-kvman.js.map +1 -0
- package/docs/i18n.md +20 -0
- package/docs/presets.md +15 -0
- package/docs/sdk.md +77 -0
- package/package.json +49 -0
- package/templates/AGENTS.md.template +13 -0
- package/templates/README.md.template +12 -0
- package/templates/extension-docs/usage.md.template +8 -0
- package/templates/src/docs.ts.template +46 -0
- package/templates/src/index.ts.template +42 -0
- package/templates/test/extension.test.ts.template +29 -0
- package/templates/web/components/Hello.vue.template +42 -0
- package/templates/web-build.ts.template +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmed Deif
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @kvman/testkit
|
|
2
|
+
|
|
3
|
+
The tools for building and testing a [kvman](https://github.com/ibndeif/kvman) extension: `createTestKernel`, which runs the real kernel in a test, and the bins `kvman-new` (a new extension project), `kvman-check`, `kvman-preset`, `kvman-preview`, and `kvman-docs`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx -p @kvman/testkit kvman-new ./notes --name @me/notes --namespace notes
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Start with [Getting started](https://github.com/ibndeif/kvman/blob/main/docs/developers/getting-started.md).
|
|
10
|
+
|
|
11
|
+
## License
|
|
12
|
+
|
|
13
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Json } from '@kvman/sdk';
|
|
2
|
+
export declare const binFailureCodes: readonly ["VALIDATION_FAILED", "NOT_FOUND", "NOT_RUNNING", "FOLDER_NOT_EMPTY", "FILE_EXISTS", "NPM_FAILED", "NO_FREE_PORT", "PREVIEW_FAILED"];
|
|
3
|
+
/** A code a bin fails with. */
|
|
4
|
+
export type BinFailureCode = (typeof binFailureCodes)[number];
|
|
5
|
+
/** A structured bin failure: `{ code, message, params? }`. */
|
|
6
|
+
export declare class BinFailure extends Error {
|
|
7
|
+
/** The machine-readable code. */
|
|
8
|
+
readonly code: BinFailureCode;
|
|
9
|
+
/** The machine-readable details, if any. */
|
|
10
|
+
readonly params: Record<string, Json> | undefined;
|
|
11
|
+
/** Wraps a code, an English message, and optional JSON params. */
|
|
12
|
+
constructor(code: BinFailureCode, message: string, params?: Record<string, Json>);
|
|
13
|
+
}
|
|
14
|
+
/** Prints a failure: one JSON object on stderr with `--json`, else the line `error: <message>`. */
|
|
15
|
+
export declare function printFailure(error: unknown, json: boolean): void;
|
|
16
|
+
//# sourceMappingURL=bin-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin-failure.d.ts","sourceRoot":"","sources":["../../src/bin/bin-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKvC,eAAO,MAAM,eAAe,+IAAgJ,CAAC;AAE7K,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,8DAA8D;AAC9D,qBAAa,UAAW,SAAQ,KAAK;IACnC,iCAAiC;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC;IAElD,kEAAkE;gBACtD,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC;CAMjF;AAED,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAQhE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The failure codes every bin of this package uses (ADR 0010, 20). kvcustomizer turns each into
|
|
2
|
+
// `kvcustomizer/<CODE>`, except `VALIDATION_FAILED`, which stays as it is.
|
|
3
|
+
export const binFailureCodes = ['VALIDATION_FAILED', 'NOT_FOUND', 'NOT_RUNNING', 'FOLDER_NOT_EMPTY', 'FILE_EXISTS', 'NPM_FAILED', 'NO_FREE_PORT', 'PREVIEW_FAILED'];
|
|
4
|
+
/** A structured bin failure: `{ code, message, params? }`. */
|
|
5
|
+
export class BinFailure extends Error {
|
|
6
|
+
/** The machine-readable code. */
|
|
7
|
+
code;
|
|
8
|
+
/** The machine-readable details, if any. */
|
|
9
|
+
params;
|
|
10
|
+
/** Wraps a code, an English message, and optional JSON params. */
|
|
11
|
+
constructor(code, message, params) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'BinFailure';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.params = params;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Prints a failure: one JSON object on stderr with `--json`, else the line `error: <message>`. */
|
|
19
|
+
export function printFailure(error, json) {
|
|
20
|
+
if (error instanceof BinFailure && json) {
|
|
21
|
+
const output = error.params === undefined ? { code: error.code, message: error.message } : { code: error.code, message: error.message, params: error.params };
|
|
22
|
+
process.stderr.write(`${JSON.stringify(output)}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
process.stderr.write(`error: ${message}\n`);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=bin-failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin-failure.js","sourceRoot":"","sources":["../../src/bin/bin-failure.ts"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,2EAA2E;AAE3E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,mBAAmB,EAAE,WAAW,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,CAAU,CAAC;AAK7K,8DAA8D;AAC9D,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,iCAAiC;IACxB,IAAI,CAAiB;IAC9B,4CAA4C;IACnC,MAAM,CAAmC;IAElD,kEAAkE;IAClE,YAAY,IAAoB,EAAE,OAAe,EAAE,MAA6B;QAC9E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,mGAAmG;AACnG,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,IAAa;IACxD,IAAI,KAAK,YAAY,UAAU,IAAI,IAAI,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;QAC9J,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Program = 'npm' | 'npx';
|
|
2
|
+
export type ProgramCommand = {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function programCommand(platform: NodeJS.Platform, program: Program, args: readonly string[]): ProgramCommand;
|
|
7
|
+
//# sourceMappingURL=program-command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program-command.d.ts","sourceRoot":"","sources":["../../src/bin/program-command.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpC,MAAM,MAAM,cAAc,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEjE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAGnH"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// How the testkit bins start npm and npx, per OS: directly on Linux and macOS; on Windows through
|
|
2
|
+
// `cmd.exe /d /s /c`, since `npm` there is a `.cmd` file.
|
|
3
|
+
export function programCommand(platform, program, args) {
|
|
4
|
+
if (platform === 'win32')
|
|
5
|
+
return { command: 'cmd.exe', args: ['/d', '/s', '/c', program, ...args] };
|
|
6
|
+
return { command: program, args: [...args] };
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=program-command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"program-command.js","sourceRoot":"","sources":["../../src/bin/program-command.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,0DAA0D;AAM1D,MAAM,UAAU,cAAc,CAAC,QAAyB,EAAE,OAAgB,EAAE,IAAuB;IACjG,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;IACpG,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Program } from './program-command.ts';
|
|
2
|
+
export type ProgramRun = {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
output: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function runProgram(program: Program, args: readonly string[], cwd: string): Promise<ProgramRun>;
|
|
8
|
+
/** The last lines of a run's output, for a failure's message. */
|
|
9
|
+
export declare function lastLines(output: string, count?: number): string;
|
|
10
|
+
//# sourceMappingURL=run-program.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-program.d.ts","sourceRoot":"","sources":["../../src/bin/run-program.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAMpE,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBtG;AAED,iEAAiE;AACjE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAE5D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { BinFailure } from "./bin-failure.js";
|
|
3
|
+
import { programCommand } from "./program-command.js";
|
|
4
|
+
export function runProgram(program, args, cwd) {
|
|
5
|
+
const start = programCommand(process.platform, program, args);
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(start.command, start.args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
8
|
+
let stdout = '';
|
|
9
|
+
let output = '';
|
|
10
|
+
child.stdout?.setEncoding('utf8').on('data', (text) => {
|
|
11
|
+
stdout += text;
|
|
12
|
+
output += text;
|
|
13
|
+
});
|
|
14
|
+
child.stderr?.setEncoding('utf8').on('data', (text) => {
|
|
15
|
+
output += text;
|
|
16
|
+
});
|
|
17
|
+
child.once('error', (error) => {
|
|
18
|
+
const missing = 'code' in error && error.code === 'ENOENT';
|
|
19
|
+
reject(missing ? new BinFailure('NPM_FAILED', `${program} isn't on the PATH; install Node.js with npm.`, { program }) : error);
|
|
20
|
+
});
|
|
21
|
+
child.once('close', (code) => {
|
|
22
|
+
resolve({ exitCode: code ?? 1, stdout, output });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** The last lines of a run's output, for a failure's message. */
|
|
27
|
+
export function lastLines(output, count = 20) {
|
|
28
|
+
return output.trimEnd().split('\n').slice(-count).join('\n');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=run-program.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-program.js","sourceRoot":"","sources":["../../src/bin/run-program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAgB,MAAM,sBAAsB,CAAC;AAQpE,MAAM,UAAU,UAAU,CAAC,OAAgB,EAAE,IAAuB,EAAE,GAAW;IAC/E,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9G,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YAC5D,MAAM,IAAI,IAAI,CAAC;YACf,MAAM,IAAI,IAAI,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YAC5D,MAAM,IAAI,IAAI,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC5B,MAAM,OAAO,GAAG,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;YAC3D,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,YAAY,EAAE,GAAG,OAAO,+CAA+C,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACjI,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3B,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,KAAK,GAAG,EAAE;IAClD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-extension.d.ts","sourceRoot":"","sources":["../../src/check/check-extension.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAgDnD,gDAAgD;AAChD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAkB9E"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ProblemError, z } from '@kvman/sdk';
|
|
4
|
+
import { createTestKernel } from "../index.js";
|
|
5
|
+
import { docsFindings } from "./docs-findings.js";
|
|
6
|
+
import { fieldFindings } from "./field-descriptions.js";
|
|
7
|
+
import { localeFindings } from "./locale-keys.js";
|
|
8
|
+
import { scanProjectTimers } from "./timer-scan.js";
|
|
9
|
+
import { uiKeys } from "./ui-keys.js";
|
|
10
|
+
// `kvman-check` (plan 09 §9.2, ADR 0009, 116): it loads the project's extension alone in a test kernel and reports a
|
|
11
|
+
// refused load (one problem at a time, since a load stops at its first error), public input fields with no
|
|
12
|
+
// description, missing locale keys, docs-pair warnings, and timer warnings.
|
|
13
|
+
const manifestSchema = z.object({ name: z.string(), kvman: z.object({}).loose() }).loose();
|
|
14
|
+
function notAProject() {
|
|
15
|
+
return { message: 'This folder has no package.json with a kvman field.', hint: 'Run kvman-check in the extension project\'s folder.', warning: false };
|
|
16
|
+
}
|
|
17
|
+
function loadFinding(problem) {
|
|
18
|
+
return { message: `The extension doesn't load: ${problem.message}`, hint: 'Fix what the kernel names; kvman-check reports the next problem once this one is gone.', warning: false };
|
|
19
|
+
}
|
|
20
|
+
function uiFailure(namespace, problem) {
|
|
21
|
+
return { message: `${namespace}.ui.get fails with ${problem.code}: ${problem.message}`, hint: 'kvwebui shows no UI from an extension whose ui.get fails; make it return its pages.', warning: false };
|
|
22
|
+
}
|
|
23
|
+
async function uiAnswer(kernel, namespace) {
|
|
24
|
+
try {
|
|
25
|
+
return { keys: uiKeys(await kernel.exec(`${namespace}.ui.get`, {})) };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof ProblemError)
|
|
29
|
+
return { failure: uiFailure(namespace, error.problem) };
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function loadedFindings(kernel, folder, name) {
|
|
34
|
+
const extension = (await kernel.exec('kernel.extensions.list', {})).find((candidate) => candidate.name === name);
|
|
35
|
+
if (extension === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
const { namespace } = extension;
|
|
38
|
+
const required = [{ key: `${namespace}.title`, usedBy: 'the Extensions page' }];
|
|
39
|
+
for (const setting of extension.settings)
|
|
40
|
+
required.push({ key: `${setting.key}.title`, usedBy: "the extension's page" });
|
|
41
|
+
const findings = fieldFindings([...extension.commands, ...extension.queries]);
|
|
42
|
+
findings.push(...(await docsFindings(kernel, namespace, extension.queries)));
|
|
43
|
+
if (extension.queries.some((query) => query.name === `${namespace}.ui.get`)) {
|
|
44
|
+
const answer = await uiAnswer(kernel, namespace);
|
|
45
|
+
if ('failure' in answer)
|
|
46
|
+
findings.push(answer.failure);
|
|
47
|
+
else
|
|
48
|
+
for (const key of answer.keys)
|
|
49
|
+
required.push({ key, usedBy: `${namespace}.ui.get` });
|
|
50
|
+
}
|
|
51
|
+
return [...findings, ...localeFindings(folder, required)];
|
|
52
|
+
}
|
|
53
|
+
/** Checks the extension project in `folder`. */
|
|
54
|
+
export async function checkExtension(folder) {
|
|
55
|
+
const manifestFile = path.join(folder, 'package.json');
|
|
56
|
+
if (!existsSync(manifestFile))
|
|
57
|
+
return [notAProject()];
|
|
58
|
+
const manifest = manifestSchema.safeParse(JSON.parse(readFileSync(manifestFile, 'utf8')));
|
|
59
|
+
if (!manifest.success)
|
|
60
|
+
return [notAProject()];
|
|
61
|
+
const timers = scanProjectTimers(folder);
|
|
62
|
+
let kernel;
|
|
63
|
+
try {
|
|
64
|
+
kernel = await createTestKernel({ extensions: [folder], logLevel: 'warn' });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof ProblemError)
|
|
68
|
+
return [loadFinding(error.problem), ...timers];
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return [...(await loadedFindings(kernel, folder, manifest.data.name)), ...timers];
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await kernel.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=check-extension.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-extension.js","sourceRoot":"","sources":["../../src/check/check-extension.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-output.d.ts","sourceRoot":"","sources":["../../src/check/check-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,cAAc,CAAC;AAK5D,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAK5E;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAExE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// How `kvman-check` prints its findings (ADR 0009, 116): readable lines for a person, or with `--json` the
|
|
2
|
+
// `[{ file?, message, hint }]` array on one line, which kvcustomizer's `ext check` reads.
|
|
3
|
+
export function readableFindings(findings) {
|
|
4
|
+
if (findings.length === 0)
|
|
5
|
+
return 'kvman-check: no findings.';
|
|
6
|
+
const lines = findings.map((finding) => `${finding.file ?? 'extension'}: ${finding.message}\n ${finding.hint}`);
|
|
7
|
+
const errors = findings.filter((finding) => !finding.warning).length;
|
|
8
|
+
return [...lines, `kvman-check: ${String(errors)} error(s), ${String(findings.length - errors)} warning(s).`].join('\n');
|
|
9
|
+
}
|
|
10
|
+
export function jsonFindings(findings) {
|
|
11
|
+
return JSON.stringify(findings.map(({ file, message, hint }) => (file === undefined ? { message, hint } : { file, message, hint })));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=check-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-output.js","sourceRoot":"","sources":["../../src/check/check-output.ts"],"names":[],"mappings":"AAEA,2GAA2G;AAC3G,0FAA0F;AAE1F,MAAM,UAAU,gBAAgB,CAAC,QAAmC;IAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,2BAA2B,CAAC;IAC9D,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,KAAK,OAAO,CAAC,OAAO,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACjH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACrE,OAAO,CAAC,GAAG,KAAK,EAAE,gBAAgB,MAAM,CAAC,MAAM,CAAC,cAAc,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3H,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAmC;IAC9D,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAW,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAChJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TestKernel } from '../index.ts';
|
|
2
|
+
import type { CheckedFinding } from './finding.ts';
|
|
3
|
+
/** The docs-pair warnings of the extension's `queries` (plan 09 §9.5): none when neither query is registered. */
|
|
4
|
+
export declare function docsFindings(kernel: TestKernel, namespace: string, queries: readonly {
|
|
5
|
+
name: string;
|
|
6
|
+
public: boolean;
|
|
7
|
+
}[]): Promise<CheckedFinding[]>;
|
|
8
|
+
//# sourceMappingURL=docs-findings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-findings.d.ts","sourceRoot":"","sources":["../../src/check/docs-findings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAiDnD,iHAAiH;AACjH,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAe1J"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ProblemError, z } from '@kvman/sdk';
|
|
2
|
+
// The docs-pair warnings (plan 09 §9.5, QA17-E30): an extension's docs reach kvcustomizer and `kvman-docs` only
|
|
3
|
+
// through both `<namespace>.docs.list` and `<namespace>.docs.get` as public queries, so a half-done or wrong pair
|
|
4
|
+
// warns instead of silently hiding the pages. Every finding here is a warning.
|
|
5
|
+
const listAnswerSchema = z.array(z.object({ topic: z.string(), title: z.string() }));
|
|
6
|
+
const pageAnswerSchema = z.object({ topic: z.string(), title: z.string(), markdown: z.string() });
|
|
7
|
+
const topicPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
8
|
+
function docsWarning(message, hint) {
|
|
9
|
+
return { message, hint, warning: true };
|
|
10
|
+
}
|
|
11
|
+
async function answeredFindings(kernel, list, get) {
|
|
12
|
+
let answer;
|
|
13
|
+
try {
|
|
14
|
+
answer = await kernel.exec(list, {});
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof ProblemError)
|
|
18
|
+
return [docsWarning(`${list} fails with ${error.problem.code}: ${error.problem.message}`, 'docs.list must answer [{ topic, title }].')];
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
const listed = listAnswerSchema.safeParse(answer);
|
|
22
|
+
if (!listed.success)
|
|
23
|
+
return [docsWarning(`${list} answers something other than [{ topic, title }].`, 'docs.list must answer [{ topic, title }].')];
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const entry of listed.data) {
|
|
26
|
+
if (!topicPattern.test(entry.topic)) {
|
|
27
|
+
findings.push(docsWarning(`The docs topic "${entry.topic}" isn't a lowercase kebab-case segment.`, 'Use lowercase letters, digits, and single hyphens, such as getting-started.'));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
let page;
|
|
31
|
+
try {
|
|
32
|
+
page = await kernel.exec(get, { topic: entry.topic });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof ProblemError) {
|
|
36
|
+
findings.push(docsWarning(`${get} fails for the topic "${entry.topic}" with ${error.problem.code}: ${error.problem.message}`, 'Every topic that docs.list answers must be answered by docs.get.'));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (!pageAnswerSchema.safeParse(page).success) {
|
|
42
|
+
findings.push(docsWarning(`${get} answers something other than { topic, title, markdown } for "${entry.topic}".`, 'Every topic that docs.list answers must be answered by docs.get.'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return findings;
|
|
46
|
+
}
|
|
47
|
+
/** The docs-pair warnings of the extension's `queries` (plan 09 §9.5): none when neither query is registered. */
|
|
48
|
+
export async function docsFindings(kernel, namespace, queries) {
|
|
49
|
+
const list = `${namespace}.docs.list`;
|
|
50
|
+
const get = `${namespace}.docs.get`;
|
|
51
|
+
const hasList = queries.some((query) => query.name === list);
|
|
52
|
+
const hasGet = queries.some((query) => query.name === get);
|
|
53
|
+
if (!hasList && !hasGet)
|
|
54
|
+
return [];
|
|
55
|
+
if (!hasList || !hasGet) {
|
|
56
|
+
const only = hasList ? list : get;
|
|
57
|
+
return [docsWarning(`Only ${only} is registered; kvcustomizer and kvman-docs show an extension's docs only when it has both ${list} and ${get}.`, 'Register the other as a public query, or remove this one.')];
|
|
58
|
+
}
|
|
59
|
+
const privateOnes = [list, get].filter((name) => !queries.some((query) => query.name === name && query.public));
|
|
60
|
+
if (privateOnes.length > 0) {
|
|
61
|
+
return privateOnes.map((name) => docsWarning(`${name} isn't public, so kvcustomizer and kvman-docs can't read it.`, 'Add public: true to its registration.'));
|
|
62
|
+
}
|
|
63
|
+
return answeredFindings(kernel, list, get);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=docs-findings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-findings.js","sourceRoot":"","sources":["../../src/check/docs-findings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CheckedFinding } from './finding.ts';
|
|
2
|
+
export type Registration = {
|
|
3
|
+
name: string;
|
|
4
|
+
public: boolean;
|
|
5
|
+
input: unknown;
|
|
6
|
+
};
|
|
7
|
+
export declare function fieldFindings(registrations: readonly Registration[]): CheckedFinding[];
|
|
8
|
+
//# sourceMappingURL=field-descriptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-descriptions.d.ts","sourceRoot":"","sources":["../../src/check/field-descriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAI7E,wBAAgB,aAAa,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,GAAG,cAAc,EAAE,CActF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from '@kvman/sdk';
|
|
2
|
+
const objectSchema = z.object({ properties: z.record(z.string(), z.object({ description: z.string().optional() }).loose()) }).loose();
|
|
3
|
+
export function fieldFindings(registrations) {
|
|
4
|
+
return registrations
|
|
5
|
+
.filter((registration) => registration.public)
|
|
6
|
+
.flatMap((registration) => {
|
|
7
|
+
const parsed = objectSchema.safeParse(registration.input);
|
|
8
|
+
if (!parsed.success)
|
|
9
|
+
return [];
|
|
10
|
+
return Object.entries(parsed.data.properties)
|
|
11
|
+
.filter(([, field]) => field.description === undefined)
|
|
12
|
+
.map(([field]) => ({
|
|
13
|
+
message: `The input field ${field} of ${registration.name} has no description.`,
|
|
14
|
+
hint: `Add .describe('…') to ${field}: forms label the field with it, and connector -h shows it.`,
|
|
15
|
+
warning: false,
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=field-descriptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-descriptions.js","sourceRoot":"","sources":["../../src/check/field-descriptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAQ/B,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;AAEtI,MAAM,UAAU,aAAa,CAAC,aAAsC;IAClE,OAAO,aAAa;SACjB,MAAM,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;SAC7C,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QACxB,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;aAC1C,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC;aACtD,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;YACjB,OAAO,EAAE,mBAAmB,KAAK,OAAO,YAAY,CAAC,IAAI,sBAAsB;YAC/E,IAAI,EAAE,yBAAyB,KAAK,6DAA6D;YACjG,OAAO,EAAE,KAAK;SACf,CAAC,CAAC,CAAC;IACR,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One thing `kvman-check` found (ADR 0009, 116): `file` is relative to the project, with `:line:col` when it has one. */
|
|
2
|
+
export type Finding = {
|
|
3
|
+
file?: string;
|
|
4
|
+
message: string;
|
|
5
|
+
hint: string;
|
|
6
|
+
};
|
|
7
|
+
/** A finding and whether it's only a warning, which doesn't fail the check. */
|
|
8
|
+
export type CheckedFinding = Finding & {
|
|
9
|
+
warning: boolean;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=finding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../../src/check/finding.ts"],"names":[],"mappings":"AAAA,0HAA0H;AAC1H,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../../src/check/finding.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckedFinding } from './finding.ts';
|
|
2
|
+
export type RequiredKey = {
|
|
3
|
+
key: string;
|
|
4
|
+
usedBy: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function localeFindings(folder: string, required: readonly RequiredKey[]): CheckedFinding[];
|
|
7
|
+
//# sourceMappingURL=locale-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-keys.d.ts","sourceRoot":"","sources":["../../src/check/locale-keys.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAoB1D,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,EAAE,CAkBjG"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from '@kvman/sdk';
|
|
4
|
+
const catalogSchema = z.record(z.string(), z.string());
|
|
5
|
+
const hint = {
|
|
6
|
+
parity: 'Add the key to every catalog, so each language shows the same texts.',
|
|
7
|
+
required: 'Add the key to every catalog; without it the UI shows the key itself.',
|
|
8
|
+
};
|
|
9
|
+
function catalogs(folder) {
|
|
10
|
+
const found = new Map([['en', new Set()]]);
|
|
11
|
+
const locales = path.join(folder, 'locales');
|
|
12
|
+
if (!existsSync(locales))
|
|
13
|
+
return found;
|
|
14
|
+
for (const file of readdirSync(locales).filter((name) => name.endsWith('.json')).sort()) {
|
|
15
|
+
const keys = Object.keys(catalogSchema.parse(JSON.parse(readFileSync(path.join(locales, file), 'utf8'))));
|
|
16
|
+
found.set(file.slice(0, -'.json'.length), new Set(keys));
|
|
17
|
+
}
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
export function localeFindings(folder, required) {
|
|
21
|
+
const shipped = catalogs(folder);
|
|
22
|
+
const everyKey = [...new Set([...shipped.values()].flatMap((keys) => [...keys]))].sort();
|
|
23
|
+
const findings = [];
|
|
24
|
+
for (const [language, keys] of shipped) {
|
|
25
|
+
const file = `locales/${language}.json`;
|
|
26
|
+
for (const key of everyKey.filter((candidate) => !keys.has(candidate))) {
|
|
27
|
+
const holders = [...shipped].filter(([, other]) => other.has(key)).map(([other]) => `locales/${other}.json`);
|
|
28
|
+
findings.push({ file, message: `${file} lacks ${key}, which ${holders.join(' and ')} has.`, hint: hint.parity, warning: false });
|
|
29
|
+
}
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const { key, usedBy } of required) {
|
|
32
|
+
if (seen.has(key) || everyKey.includes(key))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
findings.push({ file, message: `${file} lacks ${key} (used by ${usedBy}).`, hint: hint.required, warning: false });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return findings;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=locale-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-keys.js","sourceRoot":"","sources":["../../src/check/locale-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CheckedFinding } from './finding.ts';
|
|
2
|
+
export declare function blankCommentsAndStrings(source: string): string;
|
|
3
|
+
export declare function scanTimers(file: string, source: string): CheckedFinding[];
|
|
4
|
+
/** The timer warnings of the project's `src/**\/*.ts`; none when it has no `src`. */
|
|
5
|
+
export declare function scanProjectTimers(folder: string): CheckedFinding[];
|
|
6
|
+
//# sourceMappingURL=timer-scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timer-scan.d.ts","sourceRoot":"","sources":["../../src/check/timer-scan.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAwBnD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAsB9D;AAaD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAWzE;AAWD,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAGlE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
// The timer warnings (plan 02 §2.2, ADR 0009, 116): a text scan of `src/**/*.ts` with comments skipped. Every
|
|
4
|
+
// `setInterval(` warns, and every `setTimeout(` whose statement doesn't start with `await`, as
|
|
5
|
+
// `await new Promise((resolve) => setTimeout(resolve, ms))` does. It's a scan, not a parser, so it only warns.
|
|
6
|
+
const hint = {
|
|
7
|
+
interval: 'Repeated work goes to ctx.schedule; a handler\'s work ends when it returns.',
|
|
8
|
+
timeout: 'Await the timer inside the handler, or move the delayed work to ctx.execAsync or ctx.schedule.',
|
|
9
|
+
};
|
|
10
|
+
function commentEnd(source, index) {
|
|
11
|
+
if (source.startsWith('//', index)) {
|
|
12
|
+
const end = source.indexOf('\n', index);
|
|
13
|
+
return end === -1 ? source.length : end;
|
|
14
|
+
}
|
|
15
|
+
if (source.startsWith('/*', index)) {
|
|
16
|
+
const end = source.indexOf('*/', index + 2);
|
|
17
|
+
return end === -1 ? source.length : end + 2;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// Comments and string contents become spaces, so positions keep their line and column and no text inside them matches.
|
|
22
|
+
export function blankCommentsAndStrings(source) {
|
|
23
|
+
let result = '';
|
|
24
|
+
let index = 0;
|
|
25
|
+
while (index < source.length) {
|
|
26
|
+
const end = commentEnd(source, index);
|
|
27
|
+
if (end !== null) {
|
|
28
|
+
result += source.slice(index, end).replace(/[^\n]/g, ' ');
|
|
29
|
+
index = end;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const char = source[index] ?? '';
|
|
33
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
34
|
+
let close = index + 1;
|
|
35
|
+
while (close < source.length && source[close] !== char)
|
|
36
|
+
close += source[close] === '\\' ? 2 : 1;
|
|
37
|
+
result += char + source.slice(index + 1, close).replace(/[^\n]/g, ' ') + (close < source.length ? char : '');
|
|
38
|
+
index = close + 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
result += char;
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function statementStart(text, at) {
|
|
47
|
+
let start = at;
|
|
48
|
+
while (start > 0 && !';{}'.includes(text[start - 1] ?? ''))
|
|
49
|
+
start -= 1;
|
|
50
|
+
return text.slice(start, at).trimStart();
|
|
51
|
+
}
|
|
52
|
+
function position(text, at) {
|
|
53
|
+
const before = text.slice(0, at).split('\n');
|
|
54
|
+
return `${String(before.length)}:${String((before.at(-1) ?? '').length + 1)}`;
|
|
55
|
+
}
|
|
56
|
+
export function scanTimers(file, source) {
|
|
57
|
+
const text = blankCommentsAndStrings(source);
|
|
58
|
+
const findings = [];
|
|
59
|
+
for (const match of text.matchAll(/\bset(Interval|Timeout)\s*\(/g)) {
|
|
60
|
+
const interval = match[1] === 'Interval';
|
|
61
|
+
if (!interval && statementStart(text, match.index).startsWith('await'))
|
|
62
|
+
continue;
|
|
63
|
+
const call = interval ? 'setInterval' : 'setTimeout';
|
|
64
|
+
const message = interval ? `Warning: ${call} keeps running after the handler returns.` : `Warning: an unawaited ${call} outlives its handler.`;
|
|
65
|
+
findings.push({ file: `${file}:${position(text, match.index)}`, message, hint: interval ? hint.interval : hint.timeout, warning: true });
|
|
66
|
+
}
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
function sourceFiles(folder, relative) {
|
|
70
|
+
const entries = readdirSync(path.join(folder, relative), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
71
|
+
return entries.flatMap((entry) => {
|
|
72
|
+
const child = path.posix.join(relative, entry.name);
|
|
73
|
+
if (entry.isDirectory())
|
|
74
|
+
return entry.name === 'node_modules' ? [] : sourceFiles(folder, child);
|
|
75
|
+
return entry.name.endsWith('.ts') ? [child] : [];
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** The timer warnings of the project's `src/**\/*.ts`; none when it has no `src`. */
|
|
79
|
+
export function scanProjectTimers(folder) {
|
|
80
|
+
if (!existsSync(path.join(folder, 'src')))
|
|
81
|
+
return [];
|
|
82
|
+
return sourceFiles(folder, 'src').flatMap((file) => scanTimers(file, readFileSync(path.join(folder, file), 'utf8')));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=timer-scan.js.map
|