@uxf/scripts 11.122.5 → 11.124.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/README.md +1 -38
- package/package.json +12 -16
- package/src/cli-args.js +43 -0
- package/src/cli-args.test.js +61 -0
- package/src/{GitLab.js → gitlab.js} +43 -21
- package/src/gitlab.test.js +207 -0
- package/src/{GoogleChat.js → google-chat.js} +10 -12
- package/src/google-chat.test.js +95 -0
- package/src/http.js +86 -0
- package/src/http.test.js +155 -0
- package/src/sitemap.js +80 -0
- package/src/sitemap.test.js +64 -0
- package/src/{Slack.js → slack.js} +9 -8
- package/src/slack.test.js +81 -0
- package/src/uxf-i18n-namespaces-gen/dependency-tree.js +102 -0
- package/src/uxf-i18n-namespaces-gen/dependency-tree.test.js +46 -0
- package/src/uxf-i18n-namespaces-gen/index.js +62 -44
- package/src/uxf-i18n-namespaces-gen/index.test.js +32 -1
- package/src/uxf-merge-requests-notifier/cli.js +9 -12
- package/src/uxf-merge-requests-notifier/index.js +44 -10
- package/src/uxf-merge-requests-notifier/index.test.js +103 -0
- package/src/uxf-push-notifier/cli.js +14 -13
- package/src/uxf-push-notifier/index.js +32 -23
- package/src/uxf-release/index.js +3 -3
- package/src/uxf-sitemap-check/index.js +6 -2
- package/src/uxf-sitemap-check/index.test.js +2 -2
- package/src/uxf-sitemap-meta-export/index.js +3 -3
- package/bin/uxf-lunch.js +0 -8
- package/bin/uxf-unused.js +0 -9
- package/src/Logger.js +0 -12
- package/src/Sitemap.js +0 -60
- package/src/shared/load-page-imports.js +0 -60
- package/src/uxf-lunch/cli.js +0 -44
- package/src/uxf-lunch/index.js +0 -50
- package/src/uxf-unused/cli.js +0 -30
- package/src/uxf-unused/index.js +0 -59
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const { execFileSync } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// 256 MB. `rev-dep`'s own `bin.js` wrapper shells out with `execSync` and no `maxBuffer`, so it
|
|
5
|
+
// silently truncates at Node's 1 MB default and still exits 0 — we resolve the platform binary
|
|
6
|
+
// ourselves to avoid that.
|
|
7
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function resolveRevDepBinary() {
|
|
10
|
+
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
11
|
+
const request = `@rev-dep/${process.platform}-${process.arch}/bin/rev-dep${suffix}`;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return require.resolve(request, { paths: [__dirname] });
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Could not locate the rev-dep binary for your platform (${request}). ` +
|
|
18
|
+
`It ships as an optionalDependency of rev-dep — reinstall without --omit=optional.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runRevDep(cwd, tsConfig) {
|
|
24
|
+
const args = ["debug", "get-tree-for-cwd", "--cwd", cwd];
|
|
25
|
+
|
|
26
|
+
if (tsConfig) {
|
|
27
|
+
args.push("--tsconfig-json", tsConfig);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return execFileSync(resolveRevDepBinary(), args, { encoding: "utf8", maxBuffer: MAX_BUFFER });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const details = error.stderr ? `\n${error.stderr}` : "";
|
|
34
|
+
throw new Error(`rev-dep failed to build the dependency tree for ${cwd}.${details}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Turns rev-dep's tree into the shape madge's `res.obj()` returned, so the callers' traversal
|
|
40
|
+
* keeps working unchanged:
|
|
41
|
+
*
|
|
42
|
+
* { "<cwd-relative file>": ["<cwd-relative dep>" | "<bare specifier>", ...] }
|
|
43
|
+
*
|
|
44
|
+
* Imports that rev-dep could not resolve to a project file (node modules, unresolved requests)
|
|
45
|
+
* are kept as their raw specifier — callers resolve those themselves via `require.resolve`,
|
|
46
|
+
* which is how `node_modules/@uxf/**` gets picked up.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} cwd
|
|
49
|
+
* @param {{ tsConfig?: string, fileExtensions?: string[], include?: string[] }} options
|
|
50
|
+
* @returns {Record<string, string[]>}
|
|
51
|
+
*/
|
|
52
|
+
function buildDependencyTree(cwd, options = {}) {
|
|
53
|
+
const { tsConfig, fileExtensions, include } = options;
|
|
54
|
+
|
|
55
|
+
const graph = JSON.parse(runRevDep(cwd, tsConfig));
|
|
56
|
+
|
|
57
|
+
const toRelative = (filePath) => (path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath);
|
|
58
|
+
|
|
59
|
+
const extensions = fileExtensions?.map((extension) => (extension.startsWith(".") ? extension : `.${extension}`));
|
|
60
|
+
|
|
61
|
+
// `node_modules` is deliberately left to the callers' own resolution — rev-dep does not
|
|
62
|
+
// traverse into it, and it is the one place where the raw specifiers above are needed.
|
|
63
|
+
const isOutOfScope = (relativePath) =>
|
|
64
|
+
relativePath.startsWith("node_modules") || relativePath.startsWith("..") || path.isAbsolute(relativePath);
|
|
65
|
+
|
|
66
|
+
// `include` used to be madge's `excludeRegExp`: keep only files under the given prefixes.
|
|
67
|
+
const isIncluded = (relativePath) => !include?.length || include.some((prefix) => relativePath.startsWith(prefix));
|
|
68
|
+
|
|
69
|
+
const hasAllowedExtension = (relativePath) =>
|
|
70
|
+
!extensions || extensions.some((extension) => relativePath.endsWith(extension));
|
|
71
|
+
|
|
72
|
+
const isProjectFile = (relativePath) =>
|
|
73
|
+
!isOutOfScope(relativePath) && isIncluded(relativePath) && hasAllowedExtension(relativePath);
|
|
74
|
+
|
|
75
|
+
const tree = {};
|
|
76
|
+
|
|
77
|
+
for (const [file, dependencies] of Object.entries(graph)) {
|
|
78
|
+
const relativeFile = toRelative(file);
|
|
79
|
+
|
|
80
|
+
if (!isProjectFile(relativeFile)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
tree[relativeFile] = (dependencies ?? [])
|
|
85
|
+
.map((dependency) => {
|
|
86
|
+
if (dependency.resolvedTypeLabel !== "UserModule" || !dependency.id) {
|
|
87
|
+
// node module or unresolved — hand the specifier over untouched
|
|
88
|
+
return dependency.request;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const relativeDependency = toRelative(dependency.id);
|
|
92
|
+
|
|
93
|
+
return isProjectFile(relativeDependency) ? relativeDependency : undefined;
|
|
94
|
+
})
|
|
95
|
+
.filter((dependency) => typeof dependency === "string" && dependency.length > 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tree;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = buildDependencyTree;
|
|
102
|
+
module.exports.resolveRevDepBinary = resolveRevDepBinary;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const buildDependencyTree = require("./dependency-tree");
|
|
4
|
+
|
|
5
|
+
const FIXTURE_CWD = path.join(__dirname, "tests");
|
|
6
|
+
|
|
7
|
+
describe("buildDependencyTree", () => {
|
|
8
|
+
const tree = buildDependencyTree(FIXTURE_CWD, { fileExtensions: ["ts", "tsx"] });
|
|
9
|
+
|
|
10
|
+
it("keys the tree by cwd-relative paths", () => {
|
|
11
|
+
expect(Object.keys(tree)).toEqual(expect.arrayContaining(["pages/page-a.tsx", "utils.tsx"]));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("resolves relative imports to project files", () => {
|
|
15
|
+
expect(tree["pages/page-a.tsx"]).toEqual(
|
|
16
|
+
expect.arrayContaining(["components/no-index-file.tsx", "components/with-index-file/index.ts"]),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("resolves a directory import to its index file", () => {
|
|
21
|
+
expect(tree["components/with-index-file/index.ts"]).toEqual(["components/with-index-file/with-index-file.tsx"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("keeps unresolved node module imports as raw specifiers", () => {
|
|
25
|
+
// callers resolve these themselves — that is how node_modules/@uxf/** gets picked up
|
|
26
|
+
expect(tree["components/with-index-file/with-index-file.tsx"]).toEqual(
|
|
27
|
+
expect.arrayContaining(["@uxf/ui/chip", "@uxf/core/utils/noop"]),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not emit node_modules files as tree keys", () => {
|
|
32
|
+
expect(Object.keys(tree).filter((file) => file.startsWith("node_modules"))).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("honours the include filter", () => {
|
|
36
|
+
const filtered = buildDependencyTree(FIXTURE_CWD, { fileExtensions: ["ts", "tsx"], include: ["pages"] });
|
|
37
|
+
|
|
38
|
+
expect(Object.keys(filtered).every((file) => file.startsWith("pages"))).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("honours the fileExtensions filter", () => {
|
|
42
|
+
const filtered = buildDependencyTree(FIXTURE_CWD, { fileExtensions: ["ts"] });
|
|
43
|
+
|
|
44
|
+
expect(Object.keys(filtered).every((file) => file.endsWith(".ts"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
2
|
+
const buildDependencyTree = require("./dependency-tree");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { readFileSync, readdirSync, writeFileSync, statSync, existsSync } = require("fs");
|
|
5
5
|
const { findTFunctionNamespaces } = require("./utils/find-t-function-namespaces");
|
|
@@ -8,7 +8,8 @@ const join = require("node:path").join;
|
|
|
8
8
|
|
|
9
9
|
const UXF_PACKAGES_PATH = "node_modules/@uxf";
|
|
10
10
|
const FILE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "tsx", "d.ts", "mts", "cts", "d.mts", "d.cts"];
|
|
11
|
-
const
|
|
11
|
+
const DOTTED_FILE_EXTENSIONS = FILE_EXTENSIONS.map((e) => (e.startsWith(".") ? e : "." + e));
|
|
12
|
+
const TEXT_FILE_EXTENSIONS = new Set(DOTTED_FILE_EXTENSIONS);
|
|
12
13
|
const TS_CONFIG_PATH = path.resolve(process.cwd(), "tsconfig.json");
|
|
13
14
|
const TS_CONFIG = existsSync(TS_CONFIG_PATH) ? TS_CONFIG_PATH : undefined;
|
|
14
15
|
|
|
@@ -44,6 +45,38 @@ function isAllowedFile(file) {
|
|
|
44
45
|
return !file.includes("node_modules") || file.includes(UXF_PACKAGES_PATH);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Resolve an extension-less path the way TypeScript would: as a file with one of the known
|
|
50
|
+
* extensions appended, or as a directory holding an index file.
|
|
51
|
+
* @param {string} basePath
|
|
52
|
+
* @returns {string | undefined}
|
|
53
|
+
*/
|
|
54
|
+
function resolveFileCandidate(basePath) {
|
|
55
|
+
for (const extension of DOTTED_FILE_EXTENSIONS) {
|
|
56
|
+
const candidate = basePath + extension;
|
|
57
|
+
|
|
58
|
+
if (existsSync(candidate)) {
|
|
59
|
+
return candidate;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (existsSync(basePath)) {
|
|
64
|
+
if (statSync(basePath).isFile()) {
|
|
65
|
+
return basePath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const extension of DOTTED_FILE_EXTENSIONS) {
|
|
69
|
+
const candidate = path.join(basePath, "index" + extension);
|
|
70
|
+
|
|
71
|
+
if (existsSync(candidate)) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
47
80
|
/**
|
|
48
81
|
* Resolve a module specifier (bare, relative, or absolute) to a real filesystem path if possible.
|
|
49
82
|
* Falls back to the original specifier when not resolvable.
|
|
@@ -71,13 +104,9 @@ function resolveModuleSpecifier(spec) {
|
|
|
71
104
|
for (const target of targets) {
|
|
72
105
|
const mapped = target.includes("*") ? target.replace("*", middle) : target;
|
|
73
106
|
|
|
74
|
-
const
|
|
75
|
-
path.resolve(ts.baseUrl, mapped),
|
|
76
|
-
...FILE_EXTENSIONS.map((e) => path.resolve(ts.baseUrl, mapped + e)),
|
|
77
|
-
...FILE_EXTENSIONS.map((e) => path.join(ts.baseUrl, mapped, "index" + e)),
|
|
78
|
-
].find(existsSync);
|
|
107
|
+
const candidate = resolveFileCandidate(path.resolve(ts.baseUrl, mapped));
|
|
79
108
|
|
|
80
|
-
if (
|
|
109
|
+
if (candidate) return candidate;
|
|
81
110
|
}
|
|
82
111
|
}
|
|
83
112
|
} else if (spec === pattern) {
|
|
@@ -87,8 +116,19 @@ function resolveModuleSpecifier(spec) {
|
|
|
87
116
|
}
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
try {
|
|
120
|
+
const resolved = require.resolve(spec, { paths: [process.cwd()] });
|
|
121
|
+
if (isAllowedFile(resolved)) return resolved;
|
|
122
|
+
} catch {
|
|
123
|
+
// not resolvable by Node — fall through to the TypeScript probe below
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Node's resolver only knows .js, so packages published (or symlinked by a workspace) as
|
|
127
|
+
// TypeScript source are invisible to it. Probe node_modules directly for those.
|
|
128
|
+
const candidate = resolveFileCandidate(path.resolve(process.cwd(), "node_modules", spec));
|
|
129
|
+
if (candidate && isAllowedFile(candidate)) return candidate;
|
|
130
|
+
|
|
131
|
+
return spec;
|
|
92
132
|
} catch {
|
|
93
133
|
return spec;
|
|
94
134
|
}
|
|
@@ -107,20 +147,9 @@ function resolveImportFrom(spec, fromFile) {
|
|
|
107
147
|
// Relative path from the file's directory
|
|
108
148
|
if (spec.startsWith(".")) {
|
|
109
149
|
const baseDir = path.dirname(fromFile);
|
|
110
|
-
let candidate = path.resolve(baseDir, spec);
|
|
111
150
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const c = candidate + e;
|
|
115
|
-
if (existsSync(c)) return c;
|
|
116
|
-
}
|
|
117
|
-
// Try as directory index
|
|
118
|
-
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
119
|
-
for (const e of FILE_EXTENSIONS) {
|
|
120
|
-
const idx = path.join(candidate, "index" + e);
|
|
121
|
-
if (existsSync(idx)) return idx;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
151
|
+
const candidate = resolveFileCandidate(path.resolve(baseDir, spec));
|
|
152
|
+
if (candidate) return candidate;
|
|
124
153
|
|
|
125
154
|
// Fallback to Node resolver with file dir as base
|
|
126
155
|
const resolved = require.resolve(spec, { paths: [baseDir] });
|
|
@@ -191,26 +220,14 @@ const filePathToRoute = (filePath) => {
|
|
|
191
220
|
return removeTrailingSlash(route);
|
|
192
221
|
};
|
|
193
222
|
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
223
|
+
function getDependencyTree(cwd, include) {
|
|
224
|
+
// Only project files come from here — imports of node_modules/@uxf are handed over as raw
|
|
225
|
+
// specifiers and resolved by getFiles() below.
|
|
226
|
+
return buildDependencyTree(cwd, {
|
|
197
227
|
tsConfig: TS_CONFIG,
|
|
198
228
|
fileExtensions: FILE_EXTENSIONS,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return !dependency.includes("node_modules") || dependency.includes(UXF_PACKAGES_PATH);
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// If include is provided, limit traversal to those prefixes AND node_modules/@uxf.
|
|
206
|
-
if (Array.isArray(include) && include.length > 0) {
|
|
207
|
-
const allowedPrefixes = [...include, UXF_PACKAGES_PATH];
|
|
208
|
-
const searchDirs = new RegExp(`^(?!(${allowedPrefixes.join("|")}))`, "i");
|
|
209
|
-
options.excludeRegExp = [searchDirs];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const res = await madge(entries, options);
|
|
213
|
-
return res.obj();
|
|
229
|
+
include,
|
|
230
|
+
});
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
async function getFiles(entryPoint, tree) {
|
|
@@ -243,7 +260,7 @@ async function getFiles(entryPoint, tree) {
|
|
|
243
260
|
const queue = [];
|
|
244
261
|
const visited = new Set();
|
|
245
262
|
|
|
246
|
-
// Seed queue with initial files from
|
|
263
|
+
// Seed queue with initial files from the dependency tree
|
|
247
264
|
for (const file of flattenFilesOnPath) {
|
|
248
265
|
if (file && typeof file === "string") {
|
|
249
266
|
const fsPath = resolveModuleSpecifier(file);
|
|
@@ -302,8 +319,8 @@ async function main(include, output, defaultNamespaces, pagesDirectory) {
|
|
|
302
319
|
|
|
303
320
|
const pages = walk(pagesDirectory).flat(Number.POSITIVE_INFINITY);
|
|
304
321
|
|
|
305
|
-
// Build a global dependency tree rooted at project to leverage
|
|
306
|
-
const tree =
|
|
322
|
+
// Build a global dependency tree rooted at project to leverage rev-dep resolution across aliases/packages
|
|
323
|
+
const tree = getDependencyTree(process.cwd(), include);
|
|
307
324
|
|
|
308
325
|
for (const entryPoint of pages) {
|
|
309
326
|
let namespaces = [];
|
|
@@ -346,6 +363,7 @@ async function main(include, output, defaultNamespaces, pagesDirectory) {
|
|
|
346
363
|
|
|
347
364
|
module.exports = main;
|
|
348
365
|
module.exports.isAllowedFile = isAllowedFile;
|
|
366
|
+
module.exports.resolveFileCandidate = resolveFileCandidate;
|
|
349
367
|
module.exports.removeTrailingSlash = removeTrailingSlash;
|
|
350
368
|
module.exports.filePathToRoute = filePathToRoute;
|
|
351
369
|
module.exports.findNamespaces = findNamespaces;
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
/** @jest-environment node */
|
|
2
|
-
const
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
isAllowedFile,
|
|
5
|
+
removeTrailingSlash,
|
|
6
|
+
filePathToRoute,
|
|
7
|
+
findNamespaces,
|
|
8
|
+
resolveFileCandidate,
|
|
9
|
+
} = require("./index");
|
|
10
|
+
|
|
11
|
+
const FIXTURES = path.join(__dirname, "tests");
|
|
12
|
+
|
|
13
|
+
describe("resolveFileCandidate", () => {
|
|
14
|
+
it("appends the extension to an extension-less path", () => {
|
|
15
|
+
expect(resolveFileCandidate(path.join(FIXTURES, "utils"))).toBe(path.join(FIXTURES, "utils.tsx"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("resolves a directory to its index file", () => {
|
|
19
|
+
expect(resolveFileCandidate(path.join(FIXTURES, "components/with-index-file"))).toBe(
|
|
20
|
+
path.join(FIXTURES, "components/with-index-file/index.ts"),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns a path that already points at a file unchanged", () => {
|
|
25
|
+
const file = path.join(FIXTURES, "utils.tsx");
|
|
26
|
+
|
|
27
|
+
expect(resolveFileCandidate(file)).toBe(file);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns undefined when nothing matches", () => {
|
|
31
|
+
expect(resolveFileCandidate(path.join(FIXTURES, "does-not-exist"))).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
3
34
|
|
|
4
35
|
describe("isAllowedFile", () => {
|
|
5
36
|
it("allows .ts files outside node_modules", () => {
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
const { argv, env } = require("process");
|
|
2
|
+
const { hasFlag } = require("../cli-args");
|
|
2
3
|
|
|
3
4
|
const AVAILABLE_VARIANTS = ["CR", "STALE"];
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const cli = require("yargs")()
|
|
7
|
-
.command("$0", "UXF merge requests notifier", (yargs) => {
|
|
8
|
-
yargs.demandCommand(0, 0).usage(`Usage:
|
|
6
|
+
const USAGE = `Usage:
|
|
9
7
|
uxf-merge-requests-notifier [options]
|
|
10
8
|
|
|
9
|
+
Options:
|
|
10
|
+
-h, --help zobrazí tuto nápovědu
|
|
11
|
+
|
|
11
12
|
Environment variables:
|
|
12
13
|
VARIANT - optional - CR (default), STALE
|
|
13
14
|
GITLAB_TOKEN - required
|
|
14
15
|
GOOGLE_WEBHOOK_URL - required
|
|
15
|
-
CI_SERVER_URL - required - setting by GitLab CI
|
|
16
|
-
})
|
|
17
|
-
.option("h", { alias: "help", group: "Options" })
|
|
18
|
-
.strict(false)
|
|
19
|
-
.exitProcess(false);
|
|
16
|
+
CI_SERVER_URL - required - setting by GitLab CI`;
|
|
20
17
|
|
|
18
|
+
module.exports = async () => {
|
|
21
19
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (Boolean(help)) {
|
|
20
|
+
if (hasFlag(argv.slice(2), ["-h", "--help"])) {
|
|
21
|
+
console.log(USAGE);
|
|
25
22
|
return 0;
|
|
26
23
|
}
|
|
27
24
|
|
|
@@ -1,13 +1,42 @@
|
|
|
1
|
-
const GitLab = require("../
|
|
2
|
-
const GoogleChat = require("../
|
|
3
|
-
const relativeTime = require("dayjs/plugin/relativeTime");
|
|
4
|
-
const dayjs = require("dayjs");
|
|
5
|
-
require("dayjs/locale/cs");
|
|
6
|
-
dayjs.locale("cs");
|
|
7
|
-
dayjs.extend(relativeTime);
|
|
1
|
+
const GitLab = require("../gitlab");
|
|
2
|
+
const GoogleChat = require("../google-chat");
|
|
8
3
|
|
|
4
|
+
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
// `numeric: "always"` drží číselný tvar ("před 2 dny") tak, jak ho dělal dayjs.
|
|
7
|
+
// S "auto" by Intl střídalo slovní tvary ("předevčírem") jen pro některé hodnoty.
|
|
8
|
+
const RELATIVE_TIME = new Intl.RelativeTimeFormat("cs", { numeric: "always" });
|
|
9
|
+
|
|
10
|
+
/** @type {Array<[Intl.RelativeTimeFormatUnit, number]>} */
|
|
11
|
+
const RELATIVE_TIME_UNITS = [
|
|
12
|
+
["year", 365 * 86400],
|
|
13
|
+
["month", 30 * 86400],
|
|
14
|
+
["day", 86400],
|
|
15
|
+
["hour", 3600],
|
|
16
|
+
["minute", 60],
|
|
17
|
+
["second", 1],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Náhrada `dayjs(date).fromNow()` s českou lokalizací. */
|
|
21
|
+
function fromNow(date) {
|
|
22
|
+
const diffInSeconds = (new Date(date).getTime() - Date.now()) / 1000;
|
|
23
|
+
const absolute = Math.abs(diffInSeconds);
|
|
24
|
+
|
|
25
|
+
for (const [unit, seconds] of RELATIVE_TIME_UNITS) {
|
|
26
|
+
if (absolute >= seconds || unit === "second") {
|
|
27
|
+
return RELATIVE_TIME.format(Math.round(diffInSeconds / seconds), unit);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Náhrada `dayjs().diff(date, "days")` — ořezává k nule stejně jako dayjs. */
|
|
33
|
+
function daysSince(date) {
|
|
34
|
+
return Math.trunc((Date.now() - new Date(date).getTime()) / DAY_IN_MS);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Česká shoda čísla se jménem: 1 → word1, 2–4 → word2, jinak (včetně 0) → word3. */
|
|
9
38
|
function inflect(value, word1, word2, word3) {
|
|
10
|
-
return `${value} ${value === 1 ? word1 : value <= 4 ? word2 : word3}`;
|
|
39
|
+
return `${value} ${value === 1 ? word1 : value > 1 && value <= 4 ? word2 : word3}`;
|
|
11
40
|
}
|
|
12
41
|
|
|
13
42
|
function mapMergeRequests(mrs, projects) {
|
|
@@ -52,7 +81,7 @@ module.exports = async function run(variant) {
|
|
|
52
81
|
result = result.filter((mr) => mr.reviewers.length === 0);
|
|
53
82
|
break;
|
|
54
83
|
case "STALE":
|
|
55
|
-
result = result.filter((mr) =>
|
|
84
|
+
result = result.filter((mr) => daysSince(mr.updatedAt) > 30);
|
|
56
85
|
break;
|
|
57
86
|
}
|
|
58
87
|
|
|
@@ -75,7 +104,7 @@ module.exports = async function run(variant) {
|
|
|
75
104
|
card: {
|
|
76
105
|
header: {
|
|
77
106
|
title: `${mr.project.name} | ${mr.title}`,
|
|
78
|
-
subtitle: `${mr.author.name} | ${
|
|
107
|
+
subtitle: `${mr.author.name} | ${fromNow(mr.updatedAt)} | ${inflect(
|
|
79
108
|
changesCount,
|
|
80
109
|
"soubor",
|
|
81
110
|
"soubory",
|
|
@@ -98,3 +127,8 @@ module.exports = async function run(variant) {
|
|
|
98
127
|
]);
|
|
99
128
|
}
|
|
100
129
|
};
|
|
130
|
+
|
|
131
|
+
// Vystaveno kvůli testům — stejný vzorec jako v uxf-release/index.js.
|
|
132
|
+
module.exports.fromNow = fromNow;
|
|
133
|
+
module.exports.daysSince = daysSince;
|
|
134
|
+
module.exports.inflect = inflect;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { fromNow, daysSince, inflect } = require("./index");
|
|
2
|
+
|
|
3
|
+
const NOW = new Date("2026-06-15T12:00:00.000Z").getTime();
|
|
4
|
+
|
|
5
|
+
function ago(milliseconds) {
|
|
6
|
+
return new Date(NOW - milliseconds).toISOString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SECOND = 1000;
|
|
10
|
+
const MINUTE = 60 * SECOND;
|
|
11
|
+
const HOUR = 60 * MINUTE;
|
|
12
|
+
const DAY = 24 * HOUR;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.spyOn(Date, "now").mockReturnValue(NOW);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// `fromNow` a `daysSince` nahradily dayjs (`.fromNow()` a `.diff(x, "days")`),
|
|
23
|
+
// proto na ně testy — jsou to jediná místa, kde se formátování času mohlo rozejít.
|
|
24
|
+
describe("fromNow", () => {
|
|
25
|
+
it("formats seconds in Czech", () => {
|
|
26
|
+
expect(fromNow(ago(30 * SECOND))).toBe("před 30 sekundami");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("formats minutes", () => {
|
|
30
|
+
expect(fromNow(ago(5 * MINUTE))).toBe("před 5 minutami");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("formats hours", () => {
|
|
34
|
+
expect(fromNow(ago(3 * HOUR))).toBe("před 3 hodinami");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("formats days numerically rather than as a word", () => {
|
|
38
|
+
// Se `numeric: "auto"` by Intl vrátilo "předevčírem", což dayjs nedělal.
|
|
39
|
+
expect(fromNow(ago(2 * DAY))).toBe("před 2 dny");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("formats months", () => {
|
|
43
|
+
expect(fromNow(ago(40 * DAY))).toBe("před 1 měsícem");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("formats years", () => {
|
|
47
|
+
expect(fromNow(ago(400 * DAY))).toBe("před 1 rokem");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("picks the largest fitting unit", () => {
|
|
51
|
+
expect(fromNow(ago(23 * HOUR))).toBe("před 23 hodinami");
|
|
52
|
+
expect(fromNow(ago(25 * HOUR))).toBe("před 1 dnem");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("handles a future timestamp", () => {
|
|
56
|
+
expect(fromNow(new Date(NOW + 2 * DAY).toISOString())).toBe("za 2 dny");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("daysSince", () => {
|
|
61
|
+
it("counts whole days", () => {
|
|
62
|
+
expect(daysSince(ago(3 * DAY))).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("truncates a partial day, the way dayjs diff did", () => {
|
|
66
|
+
expect(daysSince(ago(3 * DAY + 23 * HOUR))).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns 0 for something that happened moments ago", () => {
|
|
70
|
+
expect(daysSince(ago(MINUTE))).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("crosses the 30 day threshold used by the STALE variant", () => {
|
|
74
|
+
expect(daysSince(ago(30 * DAY))).toBe(30);
|
|
75
|
+
expect(daysSince(ago(31 * DAY))).toBe(31);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("inflect", () => {
|
|
80
|
+
it("uses the singular for one", () => {
|
|
81
|
+
expect(inflect(1, "soubor", "soubory", "souborů")).toBe("1 soubor");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses the 2-4 form", () => {
|
|
85
|
+
expect(inflect(2, "soubor", "soubory", "souborů")).toBe("2 soubory");
|
|
86
|
+
expect(inflect(4, "soubor", "soubory", "souborů")).toBe("4 soubory");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("uses the plural genitive from five up", () => {
|
|
90
|
+
expect(inflect(5, "soubor", "soubory", "souborů")).toBe("5 souborů");
|
|
91
|
+
expect(inflect(12, "soubor", "soubory", "souborů")).toBe("12 souborů");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Regrese: původní podmínka `value <= 4` posílala nulu do tvaru pro 2–4
|
|
95
|
+
// ("0 soubory"). Čeština má pro nulu genitiv plurálu.
|
|
96
|
+
it("uses the plural genitive for zero", () => {
|
|
97
|
+
expect(inflect(0, "soubor", "soubory", "souborů")).toBe("0 souborů");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("uses the plural genitive for a value that is not a number", () => {
|
|
101
|
+
expect(inflect(NaN, "soubor", "soubory", "souborů")).toBe("NaN souborů");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
const { argv
|
|
1
|
+
const { argv } = require("process");
|
|
2
|
+
const { hasFlag, readOption } = require("../cli-args");
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const cli = require("yargs")()
|
|
5
|
-
.command("$0", "UXF push notifier", (yargs) => {
|
|
6
|
-
yargs.demandCommand(0, 0).usage(`Usage:
|
|
4
|
+
const USAGE = `Usage:
|
|
7
5
|
uxf-push-notifier [options]
|
|
8
6
|
|
|
7
|
+
Options:
|
|
8
|
+
-g, --google-chat-webhook-url URL Google Chat webhooku
|
|
9
|
+
-h, --help zobrazí tuto nápovědu
|
|
10
|
+
|
|
9
11
|
Environment variables:
|
|
10
12
|
GITLAB_TOKEN - required
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
.option("g", { alias: "google-chat-webhook-url", group: "Options" })
|
|
14
|
-
.option("h", { alias: "help", group: "Options" })
|
|
15
|
-
.strict(false)
|
|
16
|
-
.exitProcess(false);
|
|
13
|
+
`;
|
|
17
14
|
|
|
15
|
+
module.exports = async () => {
|
|
18
16
|
try {
|
|
19
|
-
const
|
|
17
|
+
const args = argv.slice(2);
|
|
20
18
|
|
|
21
|
-
if (
|
|
19
|
+
if (hasFlag(args, ["-h", "--help"])) {
|
|
20
|
+
console.log(USAGE);
|
|
22
21
|
return 0;
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
const googleChatWebhookUrl = readOption(args, ["-g", "--google-chat-webhook-url"]);
|
|
25
|
+
|
|
25
26
|
await require("./index")(googleChatWebhookUrl);
|
|
26
27
|
} catch (e) {
|
|
27
28
|
console.error(e);
|