@dokebi/cli 0.4.1 → 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.
- package/AI.md +262 -57
- package/README.md +35 -10
- package/catalog.json +2519 -257
- 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 +527 -35
- 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 +2 -0
- package/dist/check/shared-state-rules.js +114 -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 +307 -31
- 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-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/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 +29 -3
- package/dist/install-module.js +142 -13
- 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 +106 -27
- 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 +59 -4
- package/dist/npm-client.js +220 -16
- 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 +206 -20
- 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/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 +481 -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 +3016 -336
- 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/server.d.ts +50 -4
- package/dist/server.js +790 -103
- 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 +67 -0
- package/dist/transaction.js +428 -46
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +113 -4
- package/dist/verify.js +326 -28
- 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,353 @@
|
|
|
1
|
+
// The translation rules of `dokebi check` (warnings; only with @dokebi/i18n and its catalogs).
|
|
2
|
+
// The catalogs have three layers (D-0181): an app's (src/apps/<app>/locales/), the modules'
|
|
3
|
+
// (src/shared/modules/<module>/locales/) and the shared ones (src/shared/locales/); within one
|
|
4
|
+
// language an app's key wins over a module's, a module's over a shared one.
|
|
5
|
+
// i18n-missing-key a fixed key a page or server code asks for (`t('ns.a.b')`, `$t(…)`,
|
|
6
|
+
// `request.i18n.t(…)`) that the default locale of its scope lacks (code
|
|
7
|
+
// under src/apps/<app>/ reads that app's layer, the modules and the shared
|
|
8
|
+
// catalogs; other code the modules and the shared ones, community A2).
|
|
9
|
+
// @dokebi/i18n throws MISSING_MESSAGE for it everywhere but under
|
|
10
|
+
// `npx dokebi dev` (missing: auto).
|
|
11
|
+
// i18n-layer-override one key in one language in two layers: an app over a module or the
|
|
12
|
+
// shared catalogs, a module over the shared ones, or two modules. An
|
|
13
|
+
// intended override is listed in the overriding file's top-level
|
|
14
|
+
// `"$overrides": ["key", …]`; a listed key that overrides nothing is
|
|
15
|
+
// reported too. Two modules are always reported (their name order decides).
|
|
16
|
+
// i18n-layer-missing a layer has a key in one language and not in another language of the
|
|
17
|
+
// project: users of that language see a lower layer's or another
|
|
18
|
+
// language's text. One problem per layer, language and namespace.
|
|
19
|
+
// Only literal keys whose first part is a namespace of the catalogs are read (`t('auth.login')`
|
|
20
|
+
// with an auth.json); a key built at run time, one with an explicit default (`t('k', 'Text')`)
|
|
21
|
+
// and lines in a comment are not.
|
|
22
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { CONFIG_FILE, parseProjectConfig } from '../configuration.js';
|
|
25
|
+
import { debug } from '../diagnostics.js';
|
|
26
|
+
import { sources } from './form-rules.js';
|
|
27
|
+
import { readMarkers } from './source-rules.js';
|
|
28
|
+
const RULE = 'i18n-missing-key';
|
|
29
|
+
const OVERRIDE = 'i18n-layer-override';
|
|
30
|
+
const MISSING = 'i18n-layer-missing';
|
|
31
|
+
const LOCALES = 'src/shared/locales';
|
|
32
|
+
const FILES = /\.(?:vue|ts|mts|js|mjs|ejs)$/;
|
|
33
|
+
/** Files larger than this are not read (generated bundles, data). */
|
|
34
|
+
const MAX_BYTES = 1024 * 1024;
|
|
35
|
+
const CATEGORIES = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
36
|
+
/** `t(` or `$t(` (also `.t(`, `i18n.t(`) with a quoted key, then `,` or `)`. */
|
|
37
|
+
const CALL = /(^|[^\w$])\$?t\(\s*(['"`])([A-Za-z][\w-]*(?:\.[\w-]+)+)\2\s*([,)])/g;
|
|
38
|
+
/** Keys a missing-layer problem names before "and N more". */
|
|
39
|
+
const NAMED = 5;
|
|
40
|
+
/** @dokebi/i18n's rule (catalogs.ts): `other` and only plural categories with strings. */
|
|
41
|
+
function pluralMessage(value) {
|
|
42
|
+
if (value === null || typeof value !== 'object' || !Object.hasOwn(value, 'other'))
|
|
43
|
+
return false;
|
|
44
|
+
return Object.entries(value).every(([key, branch]) => CATEGORIES.has(key) && typeof branch === 'string');
|
|
45
|
+
}
|
|
46
|
+
function flatten(value, prefix, keys, top = true) {
|
|
47
|
+
if (typeof value === 'string' || pluralMessage(value)) {
|
|
48
|
+
keys.add(prefix);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
52
|
+
return;
|
|
53
|
+
for (const [key, child] of Object.entries(value)) {
|
|
54
|
+
if (top && key === '$overrides')
|
|
55
|
+
continue;
|
|
56
|
+
flatten(child, `${prefix}.${key}`, keys, false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The files of one locales folder (`<locale>/<namespace>.json`); unreadable JSON is reported. */
|
|
60
|
+
async function readLayer(root, kind, name, folder, broken) {
|
|
61
|
+
let locales;
|
|
62
|
+
try {
|
|
63
|
+
locales = await readdir(join(root, folder), { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// No catalogs folder: this layer has nothing.
|
|
67
|
+
if (error.code === 'ENOENT')
|
|
68
|
+
return undefined;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const files = new Map();
|
|
72
|
+
for (const locale of locales) {
|
|
73
|
+
if (!locale.isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
const byNamespace = new Map();
|
|
76
|
+
for (const file of await readdir(join(root, folder, locale.name), { withFileTypes: true })) {
|
|
77
|
+
if (!file.isFile() || !file.name.endsWith('.json'))
|
|
78
|
+
continue;
|
|
79
|
+
const path = `${folder}/${locale.name}/${file.name}`;
|
|
80
|
+
const text = await readFile(join(root, path), 'utf8');
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Reported, not compared: the app's start fails with INVALID_CATALOG and this cause.
|
|
87
|
+
broken.push({
|
|
88
|
+
rule: RULE,
|
|
89
|
+
severity: 'warning',
|
|
90
|
+
file: path,
|
|
91
|
+
line: 1,
|
|
92
|
+
message: `${path} is not JSON (${error.message}): its keys are not compared, and @dokebi/i18n refuses it at start (INVALID_CATALOG)`,
|
|
93
|
+
fix: 'correct the JSON of the file',
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const namespace = file.name.slice(0, -5);
|
|
98
|
+
const keys = new Set();
|
|
99
|
+
flatten(parsed, namespace, keys);
|
|
100
|
+
const listed = parsed?.$overrides;
|
|
101
|
+
const overrides = new Set(Array.isArray(listed)
|
|
102
|
+
? listed
|
|
103
|
+
.filter((key) => typeof key === 'string')
|
|
104
|
+
.map((key) => `${namespace}.${key}`)
|
|
105
|
+
: []);
|
|
106
|
+
byNamespace.set(namespace, { path, text, keys, overrides });
|
|
107
|
+
}
|
|
108
|
+
files.set(locale.name, byNamespace);
|
|
109
|
+
}
|
|
110
|
+
return files.size ? { kind, name, folder, files } : undefined;
|
|
111
|
+
}
|
|
112
|
+
/** The named folders of `parent` that have a locales folder, as layers in name order. */
|
|
113
|
+
async function namedLayers(root, parent, kind, broken) {
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await readdir(join(root, parent), { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code === 'ENOENT')
|
|
120
|
+
return [];
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const entry of entries
|
|
125
|
+
.filter((item) => item.isDirectory())
|
|
126
|
+
.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
127
|
+
const layer = await readLayer(root, kind, entry.name, `${parent}/${entry.name}/locales`, broken);
|
|
128
|
+
if (layer)
|
|
129
|
+
out.push(layer);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** The keys of a locale of a layer. */
|
|
134
|
+
function keysOf(layer, locale) {
|
|
135
|
+
const out = new Set();
|
|
136
|
+
for (const file of layer.files.get(locale)?.values() ?? [])
|
|
137
|
+
for (const key of file.keys)
|
|
138
|
+
out.add(key);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/** The keys of each locale of a scope (its layers together), for i18n-missing-key. */
|
|
142
|
+
function scopeKeys(layers, locales) {
|
|
143
|
+
const out = new Map();
|
|
144
|
+
for (const locale of locales) {
|
|
145
|
+
const keys = new Set();
|
|
146
|
+
for (const layer of layers)
|
|
147
|
+
for (const key of keysOf(layer, locale))
|
|
148
|
+
keys.add(key);
|
|
149
|
+
out.set(locale, keys);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
/** The line of `"<last part of key>":` in a catalog file, else 1. */
|
|
154
|
+
function lineOf(text, key) {
|
|
155
|
+
const name = key.slice(key.lastIndexOf('.') + 1);
|
|
156
|
+
const at = text.indexOf(`"${name}"`);
|
|
157
|
+
return at < 0 ? 1 : text.slice(0, at).split('\n').length;
|
|
158
|
+
}
|
|
159
|
+
const label = (layer) => layer.kind === 'shared' ? 'the shared catalogs' : `the ${layer.kind} ${layer.name}`;
|
|
160
|
+
/** i18n-layer-override: a key of one language in two layers, unless the overriding file lists it. */
|
|
161
|
+
export function overrideProblems(shared, modules, apps) {
|
|
162
|
+
const problems = [];
|
|
163
|
+
const below = (layers, locale, key) => layers.filter((layer) => keysOf(layer, locale).has(key));
|
|
164
|
+
const judge = (layer, lower) => {
|
|
165
|
+
for (const [locale, files] of layer.files)
|
|
166
|
+
for (const file of files.values()) {
|
|
167
|
+
for (const key of file.keys) {
|
|
168
|
+
const under = below(lower, locale, key);
|
|
169
|
+
if (!under.length || file.overrides.has(key))
|
|
170
|
+
continue;
|
|
171
|
+
problems.push({
|
|
172
|
+
rule: OVERRIDE,
|
|
173
|
+
severity: 'warning',
|
|
174
|
+
file: file.path,
|
|
175
|
+
line: lineOf(file.text, key),
|
|
176
|
+
message: `${file.path} gives ${key}, which ${under.map(label).join(' and ')} also give${under.length > 1 ? '' : 's'} in ${locale}: ${label(layer)} wins, so their text is not shown where ${label(layer)} applies`,
|
|
177
|
+
fix: `if the override is intended, add "${key.slice(key.indexOf('.') + 1)}" to the "$overrides" list at the top of ${file.path}; otherwise rename the key or remove it from one layer`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const key of file.overrides) {
|
|
181
|
+
if (file.keys.has(key) && below(lower, locale, key).length)
|
|
182
|
+
continue;
|
|
183
|
+
problems.push({
|
|
184
|
+
rule: OVERRIDE,
|
|
185
|
+
severity: 'warning',
|
|
186
|
+
file: file.path,
|
|
187
|
+
line: lineOf(file.text, '$overrides'),
|
|
188
|
+
message: `${file.path} lists ${key} in $overrides, but ${file.keys.has(key) ? `no lower layer gives it in ${locale}` : 'the file does not give it'}: the entry overrides nothing`,
|
|
189
|
+
fix: `remove "${key.slice(key.indexOf('.') + 1)}" from the "$overrides" list of ${file.path}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const base = shared ? [shared] : [];
|
|
195
|
+
for (const app of apps)
|
|
196
|
+
judge(app, [...modules, ...base]);
|
|
197
|
+
for (const module of modules)
|
|
198
|
+
judge(module, base);
|
|
199
|
+
// Two modules with one key: the first by name gives it, by chance; never silenced.
|
|
200
|
+
for (const [index, module] of modules.entries())
|
|
201
|
+
for (const [locale, files] of module.files)
|
|
202
|
+
for (const file of files.values())
|
|
203
|
+
for (const key of file.keys) {
|
|
204
|
+
const earlier = modules.slice(0, index).filter((other) => keysOf(other, locale).has(key));
|
|
205
|
+
if (!earlier.length)
|
|
206
|
+
continue;
|
|
207
|
+
problems.push({
|
|
208
|
+
rule: OVERRIDE,
|
|
209
|
+
severity: 'warning',
|
|
210
|
+
file: file.path,
|
|
211
|
+
line: lineOf(file.text, key),
|
|
212
|
+
message: `${file.path} gives ${key}, which ${earlier.map(label).join(' and ')} also give${earlier.length > 1 ? '' : 's'} in ${locale}: between modules the first by name wins, so this text is never shown`,
|
|
213
|
+
fix: `give each module its own keys (its own namespace), or move the shared wording to ${LOCALES}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return problems;
|
|
217
|
+
}
|
|
218
|
+
/** i18n-layer-missing: keys a layer gives in one language and lacks in another of the project. */
|
|
219
|
+
export function layerMissingProblems(layers, locales) {
|
|
220
|
+
const problems = [];
|
|
221
|
+
for (const layer of layers) {
|
|
222
|
+
// Every key of the layer, by namespace, in any of its languages.
|
|
223
|
+
const all = new Map();
|
|
224
|
+
for (const files of layer.files.values())
|
|
225
|
+
for (const [namespace, file] of files) {
|
|
226
|
+
const keys = all.get(namespace) ?? new Set();
|
|
227
|
+
for (const key of file.keys)
|
|
228
|
+
keys.add(key);
|
|
229
|
+
all.set(namespace, keys);
|
|
230
|
+
}
|
|
231
|
+
for (const locale of locales)
|
|
232
|
+
for (const [namespace, keys] of all) {
|
|
233
|
+
const has = layer.files.get(locale)?.get(namespace)?.keys ?? new Set();
|
|
234
|
+
const missing = [...keys].filter((key) => !has.has(key)).sort();
|
|
235
|
+
if (!missing.length)
|
|
236
|
+
continue;
|
|
237
|
+
const path = `${layer.folder}/${locale}/${namespace}.json`;
|
|
238
|
+
const named = missing.slice(0, NAMED).join(', ');
|
|
239
|
+
problems.push({
|
|
240
|
+
rule: MISSING,
|
|
241
|
+
severity: 'warning',
|
|
242
|
+
file: path,
|
|
243
|
+
line: 1,
|
|
244
|
+
message: `${label(layer)} lacks ${missing.length} key${missing.length > 1 ? 's' : ''} of ${namespace} in ${locale} that it gives in another language (${named}${missing.length > NAMED ? ` and ${missing.length - NAMED} more` : ''}): ${locale} users see a lower layer's or another language's text there`,
|
|
245
|
+
fix: `add the keys to ${path}, or remove them from the other languages of ${label(layer)}`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return problems;
|
|
250
|
+
}
|
|
251
|
+
/** The problems of one file's text against the catalogs (the path is project-relative). */
|
|
252
|
+
export function missingKeyProblems(path, text, catalogs,
|
|
253
|
+
/** dokebi-i18n defaultLocale: the end of every fallback chain. */
|
|
254
|
+
defaultLocale = 'en') {
|
|
255
|
+
// A key the default locale has never throws (every lookup chain ends there); one it lacks
|
|
256
|
+
// throws in every locale that lacks it too.
|
|
257
|
+
const fallback = catalogs.get(defaultLocale);
|
|
258
|
+
const namespaces = new Set();
|
|
259
|
+
for (const keys of catalogs.values())
|
|
260
|
+
for (const key of keys)
|
|
261
|
+
namespaces.add(key.slice(0, key.indexOf('.')));
|
|
262
|
+
const markers = readMarkers(text).filter((marker) => marker.rule === RULE);
|
|
263
|
+
const problems = [];
|
|
264
|
+
for (const [index, content] of text.split('\n').entries()) {
|
|
265
|
+
if (/^\s*(?:\/\/|\*|\/\*|<%#|<!--)/.test(content))
|
|
266
|
+
continue;
|
|
267
|
+
for (const match of content.matchAll(CALL)) {
|
|
268
|
+
const key = match[3];
|
|
269
|
+
if (!namespaces.has(key.slice(0, key.indexOf('.'))))
|
|
270
|
+
continue;
|
|
271
|
+
// An explicit default (`t('k', 'Text')`) is shown when the key is missing.
|
|
272
|
+
const after = content.slice((match.index ?? 0) + match[0].length);
|
|
273
|
+
if (match[4] === ',' && /^\s*['"`]/.test(after))
|
|
274
|
+
continue;
|
|
275
|
+
if (fallback?.has(key))
|
|
276
|
+
continue;
|
|
277
|
+
const having = [...catalogs].filter(([, keys]) => keys.has(key)).map(([locale]) => locale);
|
|
278
|
+
const line = index + 1;
|
|
279
|
+
const marker = markers.find((item) => item.line === line || item.line === line - 1);
|
|
280
|
+
if (marker?.reason.replace(/\s*[-_%]*>\s*$/, '').trim())
|
|
281
|
+
continue;
|
|
282
|
+
const namespace = key.slice(0, key.indexOf('.'));
|
|
283
|
+
problems.push({
|
|
284
|
+
rule: RULE,
|
|
285
|
+
severity: 'warning',
|
|
286
|
+
file: path,
|
|
287
|
+
line,
|
|
288
|
+
message: `${path} asks for the translation ${key}, which ${having.length ? `only the ${having.join(', ')} catalog${having.length > 1 ? 's have' : ' has'}, not the default locale ${defaultLocale}` : 'no catalog has'}: @dokebi/i18n throws MISSING_MESSAGE for it${having.length ? ' in the other locales' : ''} (under npx dokebi dev the page shows the key)${marker ? ' (the dokebi-check marker has no reason, so it does not count)' : ''}`,
|
|
289
|
+
fix: `add ${key.slice(namespace.length + 1)} to ${LOCALES}/${defaultLocale}/${namespace}.json (the fallback of every locale; or to the locales folder of its app or module), or correct the key`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return problems;
|
|
294
|
+
}
|
|
295
|
+
/** dokebi-i18n defaultLocale of dokebi.yaml (`en` when absent or unreadable: doctor reports that). */
|
|
296
|
+
async function defaultLocaleOf(root) {
|
|
297
|
+
let source;
|
|
298
|
+
try {
|
|
299
|
+
source = await readFile(join(root, CONFIG_FILE), 'utf8');
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error.code === 'ENOENT')
|
|
303
|
+
return 'en';
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const options = parseProjectConfig(source).options;
|
|
308
|
+
const section = options['dokebi-i18n'];
|
|
309
|
+
return typeof section?.defaultLocale === 'string' ? section.defaultLocale : 'en';
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
debug('check: i18n default locale not read, en assumed', { error: error.message });
|
|
313
|
+
return 'en';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** The translation rules over the catalogs and the files under `src/` (only with @dokebi/i18n). */
|
|
317
|
+
export async function missingKeysProblems(root, skip) {
|
|
318
|
+
const problems = [];
|
|
319
|
+
const shared = await readLayer(root, 'shared', 'shared', LOCALES, problems);
|
|
320
|
+
const modules = await namedLayers(root, 'src/shared/modules', 'module', problems);
|
|
321
|
+
const apps = await namedLayers(root, 'src/apps', 'app', problems);
|
|
322
|
+
const all = [...apps, ...modules, ...(shared ? [shared] : [])];
|
|
323
|
+
if (!all.length)
|
|
324
|
+
return problems;
|
|
325
|
+
const locales = [...new Set(all.flatMap((layer) => [...layer.files.keys()]))].sort();
|
|
326
|
+
problems.push(...overrideProblems(shared, modules, apps));
|
|
327
|
+
problems.push(...layerMissingProblems(all, locales));
|
|
328
|
+
const fallback = await defaultLocaleOf(root);
|
|
329
|
+
const base = [...modules, ...(shared ? [shared] : [])];
|
|
330
|
+
const scoped = new Map([['', scopeKeys(base, locales)]]);
|
|
331
|
+
for (const app of apps)
|
|
332
|
+
scoped.set(app.name, scopeKeys([app, ...base], locales));
|
|
333
|
+
let files = 0;
|
|
334
|
+
for (const path of await sources(root, 'src', FILES)) {
|
|
335
|
+
if (skip(path) || /(?:^|\/)locales\//.test(path))
|
|
336
|
+
continue;
|
|
337
|
+
const text = await readFile(join(root, path), 'utf8');
|
|
338
|
+
if (text.length > MAX_BYTES)
|
|
339
|
+
continue;
|
|
340
|
+
files++;
|
|
341
|
+
const app = /^src\/apps\/([^/]+)\//.exec(path)?.[1];
|
|
342
|
+
const catalogs = (app !== undefined && scoped.get(app)) || scoped.get('');
|
|
343
|
+
problems.push(...missingKeyProblems(path, text, catalogs, fallback));
|
|
344
|
+
}
|
|
345
|
+
debug('check: translation keys', {
|
|
346
|
+
files,
|
|
347
|
+
locales: locales.length,
|
|
348
|
+
modules: modules.length,
|
|
349
|
+
apps: apps.length,
|
|
350
|
+
problems: problems.length,
|
|
351
|
+
});
|
|
352
|
+
return problems;
|
|
353
|
+
}
|
package/dist/check/index.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { type Inspection } from '../project-inspector.js';
|
|
2
2
|
import { type Problem } from './problems.js';
|
|
3
3
|
export { type Problem, RULES, type RuleName } from './problems.js';
|
|
4
|
+
/** A `// dokebi-check: allow <rule> <reason>` marker with a reason: a problem silenced on purpose. */
|
|
5
|
+
export interface AllowedMarker {
|
|
6
|
+
readonly rule: string;
|
|
7
|
+
readonly file: string;
|
|
8
|
+
readonly line: number;
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
}
|
|
4
11
|
export interface CheckResult {
|
|
5
12
|
readonly ok: boolean;
|
|
6
13
|
readonly problems: Problem[];
|
|
7
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Every marker with a reason under src/ (M34: a database polling loop passed under one): what
|
|
16
|
+
* the project chose to silence, listed so a reader judges each reason, not only the problems.
|
|
17
|
+
*/
|
|
18
|
+
readonly allowed: readonly AllowedMarker[];
|
|
19
|
+
/** Source files the rules read (the tsconfig program under the root, and browser code). */
|
|
8
20
|
readonly files: number;
|
|
9
21
|
readonly rules: readonly string[];
|
|
10
22
|
readonly typescript: string;
|
|
@@ -19,5 +31,7 @@ export interface CheckOptions {
|
|
|
19
31
|
readonly inspection?: Inspection;
|
|
20
32
|
}
|
|
21
33
|
export declare function check(directory: string, options?: CheckOptions): Promise<CheckResult>;
|
|
34
|
+
/** The markers with a reason in the source files under src/ (generated files left out). */
|
|
35
|
+
export declare function allowedMarkers(root: string, skip: (path: string) => boolean): Promise<AllowedMarker[]>;
|
|
22
36
|
/** The human output: one block per problem, then a summary line. */
|
|
23
37
|
export declare function checkText(result: CheckResult): string;
|
package/dist/check/index.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// `dokebi check` (D-0072): the user-code convention gate of a project made by `dokebi init`.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Error rules (problems.ts), each matching only code that is wrong, with a fix per problem, and
|
|
3
|
+
// warnings (inertia-form-only D-0074; form-without-csrf, csrf-exempt-unverified D-0076 §6;
|
|
4
|
+
// responsibility-size, memory-filtered-page D-0105; vite-dev-files D-0129; formatter-migrations D-0145; console-output
|
|
5
|
+
// D-0151; background-timer is
|
|
6
|
+
// an error) that report without failing. The security rules run only when the plugin they protect is installed.
|
|
4
7
|
// Project rules reuse doctor's inspection (one source for CLI-owned files and plugin checks);
|
|
5
8
|
// source rules walk the AST of the project's own TypeScript 7 program (typescript.ts); the form
|
|
6
|
-
// rule reads the text of src/ (Vue files are not in the program, form-rules.ts).
|
|
7
|
-
|
|
9
|
+
// rule reads the text of src/ (Vue files are not in the program, form-rules.ts). The .vue scripts
|
|
10
|
+
// and the .js under views/ go to a second program of the same tsgo (browser-sources.ts, D-0120)
|
|
11
|
+
// for empty-catch, missing-cause and floating-promise.
|
|
12
|
+
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
13
|
+
import { extname, join } from 'node:path';
|
|
8
14
|
import { loadCatalog } from '../catalog.js';
|
|
9
15
|
import { debug } from '../diagnostics.js';
|
|
10
16
|
import { ProjectInspector } from '../project-inspector.js';
|
|
11
|
-
import {
|
|
17
|
+
import { assetProblems } from './asset-rules.js';
|
|
18
|
+
import { browserSources } from './browser-sources.js';
|
|
19
|
+
import { csrfFormsProblems, formProblems } from './form-rules.js';
|
|
20
|
+
import { formatterProblems } from './formatter-rules.js';
|
|
21
|
+
import { missingKeysProblems } from './i18n-rules.js';
|
|
12
22
|
import { DokebiImports } from './packages.js';
|
|
13
|
-
import { byPlace, RULES } from './problems.js';
|
|
23
|
+
import { byPlace, MARKABLE, RULES } from './problems.js';
|
|
14
24
|
import { projectProblems } from './project-rules.js';
|
|
15
|
-
import {
|
|
25
|
+
import { sealHeadsProblems } from './seal-rules.js';
|
|
26
|
+
import { sharedStateProblems } from './shared-state-rules.js';
|
|
27
|
+
import { readMarkers, sourceProblems } from './source-rules.js';
|
|
16
28
|
import { openTypeScript } from './typescript.js';
|
|
29
|
+
import { viteConfigProblems } from './vite-rules.js';
|
|
17
30
|
export { RULES } from './problems.js';
|
|
18
31
|
/** CLI-generated TypeScript: judged by doctor (generated-file), not by the source rules. */
|
|
19
32
|
const GENERATED_SOURCES = new Set(['src/dokebi.generated.ts']);
|
|
@@ -22,18 +35,31 @@ export async function check(directory, options = {}) {
|
|
|
22
35
|
const root = await realpath(directory);
|
|
23
36
|
const inspection = options.inspection ?? (await new ProjectInspector().inspect(root, { runChecks: true }));
|
|
24
37
|
const problems = await projectProblems(root, inspection);
|
|
25
|
-
const
|
|
38
|
+
const installed = new Set(inspection.plugins.flatMap((item) => item.installed?.id ?? []));
|
|
39
|
+
const skip = (path) => GENERATED_SOURCES.has(path);
|
|
40
|
+
const typescript = await openTypeScript(root, 'tsconfig.json', await browserSources(root, skip));
|
|
26
41
|
let files = 0;
|
|
27
42
|
try {
|
|
28
|
-
const found = await sourceProblems(typescript, root, new DokebiImports(await loadCatalog()),
|
|
43
|
+
const found = await sourceProblems(typescript, root, new DokebiImports(await loadCatalog()), skip, installed);
|
|
29
44
|
problems.push(...found.problems);
|
|
30
45
|
files = found.files;
|
|
31
46
|
}
|
|
32
47
|
finally {
|
|
33
48
|
typescript.close();
|
|
34
49
|
}
|
|
35
|
-
problems.push(...(await formProblems(root,
|
|
50
|
+
problems.push(...(await formProblems(root, skip)).problems);
|
|
51
|
+
if (installed.has('dokebi-csrf'))
|
|
52
|
+
problems.push(...(await csrfFormsProblems(root)).problems);
|
|
53
|
+
problems.push(...(await viteConfigProblems(root, installed)));
|
|
54
|
+
problems.push(...(await formatterProblems(root)));
|
|
55
|
+
problems.push(...(await sharedStateProblems(root, installed)));
|
|
56
|
+
problems.push(...(await assetProblems(root)));
|
|
57
|
+
if (installed.has('dokebi-app'))
|
|
58
|
+
problems.push(...(await sealHeadsProblems(root, skip)));
|
|
59
|
+
if (installed.has('dokebi-i18n'))
|
|
60
|
+
problems.push(...(await missingKeysProblems(root, skip)));
|
|
36
61
|
problems.sort(byPlace);
|
|
62
|
+
const allowed = await allowedMarkers(root, skip);
|
|
37
63
|
const ms = {
|
|
38
64
|
total: Math.round(performance.now() - began),
|
|
39
65
|
typescript: typescript.loadMs,
|
|
@@ -42,23 +68,75 @@ export async function check(directory, options = {}) {
|
|
|
42
68
|
debug('check: finished', {
|
|
43
69
|
root,
|
|
44
70
|
problems: problems.length,
|
|
71
|
+
allowed: allowed.length,
|
|
45
72
|
warnings: problems.length - errors,
|
|
46
73
|
files,
|
|
47
74
|
...ms,
|
|
48
75
|
});
|
|
49
76
|
return {
|
|
50
|
-
// Warnings (inertia-form-only) are reported but never fail check or verify.
|
|
77
|
+
// Warnings (inertia-form-only, the CSRF ones) are reported but never fail check or verify.
|
|
51
78
|
ok: errors === 0,
|
|
52
79
|
problems,
|
|
80
|
+
allowed,
|
|
53
81
|
files,
|
|
54
82
|
rules: RULES,
|
|
55
83
|
typescript: typescript.version,
|
|
56
84
|
ms,
|
|
57
85
|
};
|
|
58
86
|
}
|
|
87
|
+
const MARKED_SOURCES = new Set([
|
|
88
|
+
'.ts',
|
|
89
|
+
'.mts',
|
|
90
|
+
'.cts',
|
|
91
|
+
'.js',
|
|
92
|
+
'.mjs',
|
|
93
|
+
'.cjs',
|
|
94
|
+
'.vue',
|
|
95
|
+
'.ejs',
|
|
96
|
+
'.html',
|
|
97
|
+
]);
|
|
98
|
+
/** The markers with a reason in the source files under src/ (generated files left out). */
|
|
99
|
+
export async function allowedMarkers(root, skip) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(join(root, 'src'), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === 'ENOENT')
|
|
106
|
+
return [];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const found = [];
|
|
110
|
+
for (const entry of entries.sort()) {
|
|
111
|
+
const file = `src/${entry.split('\\').join('/')}`;
|
|
112
|
+
if (!MARKED_SOURCES.has(extname(file)) || skip(file) || /(^|\/)node_modules\//.test(file))
|
|
113
|
+
continue;
|
|
114
|
+
let text;
|
|
115
|
+
try {
|
|
116
|
+
text = await readFile(join(root, file), 'utf8');
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// A folder named like a source file, or a file removed meanwhile: nothing to list.
|
|
120
|
+
const code = error.code;
|
|
121
|
+
if (code === 'EISDIR' || code === 'ENOENT')
|
|
122
|
+
continue;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (!text.includes('dokebi-check:'))
|
|
126
|
+
continue;
|
|
127
|
+
for (const marker of readMarkers(text)) {
|
|
128
|
+
const reason = marker.reason.replace(/\s*(\*\/|-->|%>)\s*$/, '').trim();
|
|
129
|
+
if (reason && MARKABLE.has(marker.rule))
|
|
130
|
+
found.push({ rule: marker.rule, file, line: marker.line, reason });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return found;
|
|
134
|
+
}
|
|
59
135
|
/** The human output: one block per problem, then a summary line. */
|
|
60
136
|
export function checkText(result) {
|
|
61
137
|
const lines = result.problems.map((problem) => `${problem.file}${problem.line ? `:${problem.line}` : ''} ${problem.rule}${problem.severity === 'warning' ? ' (warning)' : ''} ${problem.message}\n fix: ${problem.fix}`);
|
|
138
|
+
for (const marker of result.allowed)
|
|
139
|
+
lines.push(`${marker.file}:${marker.line} allowed ${marker.rule} ${marker.reason}`);
|
|
62
140
|
const timing = `${result.files} files, ${result.ms.total} ms (TypeScript ${result.typescript} load ${result.ms.typescript} ms)`;
|
|
63
141
|
const warnings = result.problems.filter((problem) => problem.severity === 'warning').length;
|
|
64
142
|
const errors = result.problems.length - warnings;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
|
|
2
|
+
type Rule = 'mail-send-in-request' | 'mail-secret-in-data';
|
|
3
|
+
export declare function mailRules(file: TsSourceFile, kinds: Kinds, report: (node: TsNode, rule: Rule, message: string, fix: string) => void): void;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// D-0078 MP4: conservative source heuristics, only with @dokebi/mail installed.
|
|
2
|
+
import { nameWords } from './security-rules.js';
|
|
3
|
+
import { field, nodes, property, } from './typescript.js';
|
|
4
|
+
const SECRETS = new Set(['token', 'password', 'passwd', 'secret', 'url', 'link']);
|
|
5
|
+
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'route']);
|
|
6
|
+
export function mailRules(file, kinds, report) {
|
|
7
|
+
const name = (node) => {
|
|
8
|
+
if (!node)
|
|
9
|
+
return '';
|
|
10
|
+
const key = field(node, 'name');
|
|
11
|
+
return String(property(key ?? node, 'text') ?? '');
|
|
12
|
+
};
|
|
13
|
+
const routeHandlers = new Set();
|
|
14
|
+
const collect = (node) => {
|
|
15
|
+
if (node.kind === kinds.CallExpression && METHODS.has(name(field(node, 'expression'))))
|
|
16
|
+
for (const arg of nodes(node, 'arguments'))
|
|
17
|
+
if (arg.kind === kinds.Identifier)
|
|
18
|
+
routeHandlers.add(name(arg));
|
|
19
|
+
node.forEachChild(collect);
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
collect(file);
|
|
23
|
+
const walk = (node, inRequest = false) => {
|
|
24
|
+
const functional = node.kind === kinds.ArrowFunction ||
|
|
25
|
+
node.kind === kinds.FunctionExpression ||
|
|
26
|
+
node.kind === kinds.FunctionDeclaration;
|
|
27
|
+
const request = inRequest ||
|
|
28
|
+
(functional &&
|
|
29
|
+
(routeHandlers.has(name(node)) ||
|
|
30
|
+
nodes(node, 'parameters').some((p) => /^(request|req)$/.test(name(p)))));
|
|
31
|
+
if (node.kind === kinds.CallExpression) {
|
|
32
|
+
const callee = field(node, 'expression');
|
|
33
|
+
const method = name(callee);
|
|
34
|
+
const receiver = callee && field(callee, 'expression');
|
|
35
|
+
// mail, app.mail, this.mail and notificationMailer; no unrelated socket.send calls.
|
|
36
|
+
const isMail = nameWords(name(receiver).replace(/^#/, '')).some((word) => word === 'mail' || word === 'mailer');
|
|
37
|
+
if (isMail && (method === 'send' || method === 'queue')) {
|
|
38
|
+
if (request && method === 'send')
|
|
39
|
+
report(node, 'mail-send-in-request', 'Mail delivery blocks this request on the transport.', 'Use await mail.queue(message) with a provisioned @dokebi/job queue.');
|
|
40
|
+
const message = nodes(node, 'arguments')[0];
|
|
41
|
+
if (message?.kind === kinds.ObjectLiteralExpression) {
|
|
42
|
+
const data = nodes(message, 'properties').find((item) => name(item) === 'data');
|
|
43
|
+
const value = data && field(data, 'initializer');
|
|
44
|
+
const secrets = (value) => {
|
|
45
|
+
if (value.kind === kinds.ObjectLiteralExpression)
|
|
46
|
+
for (const item of nodes(value, 'properties')) {
|
|
47
|
+
if (nameWords(name(item)).some((word) => SECRETS.has(word)))
|
|
48
|
+
report(item, 'mail-secret-in-data', 'A token, password or link-like field is in unsealed mail data.', 'Move sensitive values to sealed; templates read them from data after decryption.');
|
|
49
|
+
const nested = field(item, 'initializer');
|
|
50
|
+
if (nested)
|
|
51
|
+
secrets(nested);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
if (value)
|
|
56
|
+
secrets(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Inline handlers without named request parameters are still request handlers.
|
|
60
|
+
const route = METHODS.has(method) && receiver !== undefined;
|
|
61
|
+
// The callee too: in `mail.send(m).then(…)` the send is the receiver of `then`.
|
|
62
|
+
if (callee)
|
|
63
|
+
walk(callee, request);
|
|
64
|
+
for (const child of nodes(node, 'arguments'))
|
|
65
|
+
walk(child, request || route);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
node.forEachChild((child) => walk(child, request));
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
walk(file);
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
|
|
2
|
+
/** One point listed in a `contributes` array, with the module specifier it was imported from. */
|
|
3
|
+
export interface ModulePoint {
|
|
4
|
+
readonly node: TsNode;
|
|
5
|
+
readonly module: string;
|
|
6
|
+
readonly point: string;
|
|
7
|
+
readonly specifier: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The points of every `defineModule({ contributes: [...] })` of the file whose `defineModule`
|
|
11
|
+
* comes from @dokebi/module, when the point is a name (or `ns.name`) imported from a package.
|
|
12
|
+
*/
|
|
13
|
+
export declare function modulePoints(file: TsSourceFile, kinds: Kinds): ModulePoint[];
|