@uxf/scripts 11.123.0 → 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 -1
- package/package.json +4 -5
- 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/src/Logger.js +0 -12
- package/src/Sitemap.js +0 -60
|
@@ -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);
|
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
const { create } = require("axios");
|
|
2
1
|
const process = require("process");
|
|
2
|
+
const { request } = require("../http");
|
|
3
3
|
|
|
4
4
|
const { GITLAB_TOKEN, CI_SERVER_URL, CI_COMMIT_SHA, CI_COMMIT_BEFORE_SHA, CI_PROJECT_ID, CI_COMMIT_REF_NAME } =
|
|
5
5
|
process.env;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} url
|
|
9
|
+
* @param {{params?: Record<string, any>}} options
|
|
10
|
+
*/
|
|
11
|
+
function gitlabRequest(url, options = {}) {
|
|
12
|
+
return request(url, {
|
|
13
|
+
...options,
|
|
14
|
+
baseUrl: `${CI_SERVER_URL}/api/v4`,
|
|
15
|
+
headers: { Authorization: `Bearer ${GITLAB_TOKEN}` },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
function findCurrentlyMergedMergeRequest() {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
return gitlabRequest(`/projects/${CI_PROJECT_ID}/merge_requests`, {
|
|
21
|
+
params: {
|
|
22
|
+
state: "merged",
|
|
23
|
+
target_branch: CI_COMMIT_REF_NAME,
|
|
24
|
+
order_by: "updated_at",
|
|
25
|
+
sort: "desc",
|
|
26
|
+
},
|
|
27
|
+
}).then((response) => (response.data?.[0]?.sha === CI_COMMIT_SHA ? response.data[0] : null));
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
function getApprovalUserNames(iid) {
|
|
25
|
-
return
|
|
26
|
-
.
|
|
27
|
-
|
|
31
|
+
return gitlabRequest(`/projects/${CI_PROJECT_ID}/merge_requests/${iid}/approvals`).then((response) =>
|
|
32
|
+
response.data.approved_by.map((item) => item.user.name),
|
|
33
|
+
);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function getPushedCommits() {
|
|
@@ -35,9 +41,9 @@ function getPushedCommits() {
|
|
|
35
41
|
return Promise.resolve([]);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
return gitlabRequest(`/projects/${CI_PROJECT_ID}/repository/compare`, {
|
|
45
|
+
params: { from: CI_COMMIT_BEFORE_SHA, to: CI_COMMIT_SHA },
|
|
46
|
+
}).then((response) => response.data.commits);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
module.exports = async function (googleChatWebhookUrl) {
|
|
@@ -54,15 +60,18 @@ Autor: ${mr.author.name}
|
|
|
54
60
|
Schválil: ${isApproved ? approvalUserNames.join(", ") : "*bez schválení*"}
|
|
55
61
|
Zamergoval: ${mr.merged_by.name}`;
|
|
56
62
|
|
|
57
|
-
await
|
|
63
|
+
await request(googleChatWebhookUrl, { method: "POST", body: { text } });
|
|
58
64
|
} else {
|
|
59
65
|
const commits = (await getPushedCommits()).map(
|
|
60
66
|
(commit) => `${commit.author_name} - <${commit.web_url}|${commit.title}>`,
|
|
61
67
|
);
|
|
62
68
|
|
|
63
|
-
await
|
|
64
|
-
|
|
69
|
+
await request(googleChatWebhookUrl, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: {
|
|
72
|
+
text: `❗ Bylo pushnuto do developu.
|
|
65
73
|
${commits.join("\n")}`,
|
|
74
|
+
},
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
};
|
package/src/uxf-release/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const GitLab = require("../
|
|
2
|
-
const Slack = require("../
|
|
3
|
-
const GoogleChat = require("../
|
|
1
|
+
const GitLab = require("../gitlab");
|
|
2
|
+
const Slack = require("../slack");
|
|
3
|
+
const GoogleChat = require("../google-chat");
|
|
4
4
|
const parseCommitMessage = require("./utils/parse-commit-message");
|
|
5
5
|
|
|
6
6
|
function generateSlackCommitMessage(commit) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const Sitemap = require("../
|
|
1
|
+
const Sitemap = require("../sitemap");
|
|
2
2
|
const { performance } = require("perf_hooks");
|
|
3
3
|
const { stdout } = require("process");
|
|
4
4
|
const cheerio = require("cheerio");
|
|
5
|
-
const GoogleChat = require("../
|
|
5
|
+
const GoogleChat = require("../google-chat");
|
|
6
6
|
const robotsTxtParser = require("robots-txt-parser");
|
|
7
7
|
|
|
8
8
|
const got = (url, init) => import("got").then((mod) => mod.default(url, init));
|
|
@@ -78,6 +78,10 @@ function fetcher(url, options) {
|
|
|
78
78
|
return got(url, {
|
|
79
79
|
throwHttpErrors: false,
|
|
80
80
|
decompress: false,
|
|
81
|
+
// BEZPEČNOST: vypnuté ověřování TLS certifikátu — platí na VŠECHNY procházené
|
|
82
|
+
// URL, tedy i externí a produkční, ne jen na staging se self-signed certem.
|
|
83
|
+
// Crawler tím nepozná podvržený certifikát (MITM). Čistší by bylo podmínit to
|
|
84
|
+
// env proměnnou nebo přidat CA do trust storu. Stejná poznámka je v sitemap.js.
|
|
81
85
|
https: {
|
|
82
86
|
rejectUnauthorized: false,
|
|
83
87
|
},
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @jest-environment node
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
jest.mock("../
|
|
6
|
-
jest.mock("../
|
|
5
|
+
jest.mock("../sitemap");
|
|
6
|
+
jest.mock("../google-chat");
|
|
7
7
|
jest.mock("cheerio");
|
|
8
8
|
jest.mock("got");
|
|
9
9
|
jest.mock("robots-txt-parser", () => () => ({ useRobotsFor: jest.fn(), canCrawl: jest.fn() }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const Sitemap = require("../
|
|
1
|
+
const Sitemap = require("../sitemap");
|
|
2
2
|
const cheerio = require("cheerio");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
|
|
@@ -17,9 +17,9 @@ module.exports = async function run() {
|
|
|
17
17
|
for (const url of urls) {
|
|
18
18
|
process.stdout.write(`${++i} / ${urls.length} ${url} \n`);
|
|
19
19
|
try {
|
|
20
|
-
const
|
|
20
|
+
const body = await Sitemap.fetchPage(url);
|
|
21
21
|
|
|
22
|
-
const $ = cheerio.load(
|
|
22
|
+
const $ = cheerio.load(body, { xmlMode: true, decodeEntities: false });
|
|
23
23
|
|
|
24
24
|
let title = "";
|
|
25
25
|
let ogTitle = "";
|