@crowdin/app-project-module 3.2.2 → 3.4.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.
Files changed (34) hide show
  1. package/out/middlewares/integration-access-denied.d.ts +6 -2
  2. package/out/middlewares/integration-access-denied.js +25 -11
  3. package/out/modules/about.js +2 -10
  4. package/out/modules/api/api.js +7 -0
  5. package/out/modules/api/components.d.ts +6 -1
  6. package/out/modules/api/components.js +6 -1
  7. package/out/modules/integration/handlers/crowdin-update.js +33 -6
  8. package/out/modules/integration/handlers/integration-update.js +1 -1
  9. package/out/modules/integration/handlers/job-list.d.ts +8 -2
  10. package/out/modules/integration/handlers/job-list.js +30 -11
  11. package/out/modules/integration/handlers/main.js +15 -5
  12. package/out/modules/integration/types.d.ts +8 -0
  13. package/out/modules/integration/util/types.d.ts +2 -0
  14. package/out/static/css/styles.css +1 -1
  15. package/out/static/img/crowdin_small.svg +8 -7
  16. package/out/static/ui/about.bundle.js +11 -1
  17. package/out/static/ui/about.bundle.js.map +1 -1
  18. package/out/static/ui/error.bundle.js +8 -8
  19. package/out/static/ui/error.bundle.js.map +1 -1
  20. package/out/static/ui/form.bundle.js.map +1 -1
  21. package/out/static/ui/login.bundle.js +4 -4
  22. package/out/static/ui/login.bundle.js.map +1 -1
  23. package/out/static/ui/main.bundle.js +750 -687
  24. package/out/static/ui/main.bundle.js.map +1 -1
  25. package/out/static/ui/subscription.bundle.js +1 -1
  26. package/out/static/ui/subscription.bundle.js.map +1 -1
  27. package/out/storage/drizzle/index.js +6 -1
  28. package/out/util/index.d.ts +1 -0
  29. package/out/util/index.js +16 -1
  30. package/out/util/logo.d.ts +4 -0
  31. package/out/util/logo.js +47 -0
  32. package/out/util/static-files.d.ts +4 -1
  33. package/out/util/static-files.js +26 -4
  34. package/package.json +2 -1
@@ -906,9 +906,14 @@ class DrizzleStorage {
906
906
  }
907
907
  getAllJobs(params) {
908
908
  return __awaiter(this, void 0, void 0, function* () {
909
+ var _a;
909
910
  yield this.migrationPromise;
910
911
  const items = yield this.db.query.job.findMany({
911
- where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(this.schema.job.integration_id, params.integrationId), (0, drizzle_orm_1.eq)(this.schema.job.crowdin_id, params.crowdinId)),
912
+ where: (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(this.schema.job.integration_id, params.integrationId), (0, drizzle_orm_1.eq)(this.schema.job.crowdin_id, params.crowdinId), params.finished === undefined
913
+ ? undefined
914
+ : params.finished
915
+ ? (0, drizzle_orm_1.isNotNull)(this.schema.job.finished_at)
916
+ : (0, drizzle_orm_1.isNull)(this.schema.job.finished_at), ((_a = params.excludeTypes) === null || _a === void 0 ? void 0 : _a.length) ? (0, drizzle_orm_1.notInArray)(this.schema.job.type, params.excludeTypes) : undefined),
912
917
  orderBy: (0, drizzle_orm_1.desc)(this.schema.job.created_at),
913
918
  limit: params.limit,
914
919
  offset: params.offset,
@@ -30,6 +30,7 @@ export declare function getFormattedDate({ date, userTimezone }: {
30
30
  date: Date | number;
31
31
  userTimezone: string | undefined;
32
32
  }): string;
33
+ export declare function escapeHtml(value: string): string;
33
34
  export declare function kebabCase(str?: string): string;
34
35
  export declare function snakeCase(str?: string): string;
35
36
  export declare function getEnvInt(name: string, defaultValue: number): number;
package/out/util/index.js CHANGED
@@ -59,6 +59,7 @@ exports.prepareFormDataMetadataIdFromContext = prepareFormDataMetadataIdFromCont
59
59
  exports.prepareFormDataMetadataId = prepareFormDataMetadataId;
60
60
  exports.validateEmail = validateEmail;
61
61
  exports.getFormattedDate = getFormattedDate;
62
+ exports.escapeHtml = escapeHtml;
62
63
  exports.kebabCase = kebabCase;
63
64
  exports.snakeCase = snakeCase;
64
65
  exports.getEnvInt = getEnvInt;
@@ -69,6 +70,7 @@ const types_1 = require("../types");
69
70
  const jsx_renderer_1 = require("./jsx-renderer");
70
71
  const logger_1 = require("./logger");
71
72
  const static_files_1 = require("./static-files");
73
+ const logo_1 = require("./logo");
72
74
  const token_1 = require("./app-functions/token");
73
75
  /**
74
76
  * Extract file name from path (works in both Node.js and Cloudflare Workers)
@@ -252,7 +254,12 @@ function serveLogo(config, moduleConfig) {
252
254
  }
253
255
  const imagePath = config.imagePath;
254
256
  const fileName = basename(imagePath);
255
- const fileHandler = (0, static_files_1.serveFile)(config, imagePath);
257
+ // Generated letter logo when the logo file is missing
258
+ const svg = (0, logo_1.generateLetterLogoSvg)(config.name);
259
+ const fileHandler = (0, static_files_1.serveFile)(config, imagePath, (req, res) => {
260
+ res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
261
+ res.send(svg);
262
+ });
256
263
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
257
264
  // Match exact paths: /logo.png (backwards-compatible) or /{actual-file-name}
258
265
  if (req.path === '/logo.png' || req.path === `/${fileName}`) {
@@ -326,6 +333,14 @@ function getFormattedDate({ date, userTimezone }) {
326
333
  timeZone: userTimezone,
327
334
  }).format(date);
328
335
  }
336
+ function escapeHtml(value) {
337
+ return value
338
+ .replace(/&/g, '&')
339
+ .replace(/</g, '&lt;')
340
+ .replace(/>/g, '&gt;')
341
+ .replace(/"/g, '&quot;')
342
+ .replace(/'/g, '&#39;');
343
+ }
329
344
  function kebabCase(str) {
330
345
  if (!str) {
331
346
  return '';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Generated fallback logo (rounded square with the first letter of the app name)
3
+ */
4
+ export declare function generateLetterLogoSvg(name?: string): string;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * Generated fallback logo (rounded square with the first letter of the app name)
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateLetterLogoSvg = generateLetterLogoSvg;
7
+ const index_1 = require("./index");
8
+ const PALETTE = [
9
+ { background: '#DBEAFE', foreground: '#1E3A8A' },
10
+ { background: '#DCFCE7', foreground: '#14532D' },
11
+ { background: '#FEF3C7', foreground: '#78350F' },
12
+ { background: '#FCE7F3', foreground: '#831843' },
13
+ { background: '#EDE9FE', foreground: '#4C1D95' },
14
+ { background: '#CFFAFE', foreground: '#164E63' },
15
+ { background: '#FFE4E6', foreground: '#881337' },
16
+ { background: '#E0E7FF', foreground: '#312E81' },
17
+ ];
18
+ const NEUTRAL = { background: '#E2E8F0', foreground: '#334155' };
19
+ function getLogoColors(name) {
20
+ // FNV-1a
21
+ let hash = 0x811c9dc5;
22
+ for (let i = 0; i < name.length; i++) {
23
+ hash ^= name.charCodeAt(i);
24
+ hash = Math.imul(hash, 0x01000193) >>> 0;
25
+ }
26
+ return PALETTE[hash % PALETTE.length];
27
+ }
28
+ function generateLetterLogoSvg(name) {
29
+ const text = (name || '').trim();
30
+ const letter = text.charAt(0).toUpperCase();
31
+ const colors = letter ? getLogoColors(text) : NEUTRAL;
32
+ const content = letter
33
+ ? `<text x="16" y="16" text-anchor="middle" dominant-baseline="central"
34
+ font-family="system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif" font-size="19" font-weight="600"
35
+ fill="${colors.foreground}">${(0, index_1.escapeHtml)(letter)}</text>`
36
+ : `<g fill="${colors.foreground}">
37
+ <rect x="8" y="8" width="7" height="7" rx="1.5"/>
38
+ <rect x="17" y="8" width="7" height="7" rx="1.5"/>
39
+ <rect x="8" y="17" width="7" height="7" rx="1.5"/>
40
+ <rect x="17" y="17" width="7" height="7" rx="1.5"/>
41
+ </g>`;
42
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
43
+ <title>${(0, index_1.escapeHtml)(text || 'App')}</title>
44
+ <rect width="32" height="32" rx="7" fill="${colors.background}"/>
45
+ ${content}
46
+ </svg>`;
47
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import express, { Request, Response } from 'express';
6
6
  import { Config, UnauthorizedConfig } from '../types';
7
+ type MissingFileHandler = (req: Request, res: Response) => void;
7
8
  /**
8
9
  * Serve static directory with dual compatibility
9
10
  * @param config - App configuration
@@ -14,5 +15,7 @@ export declare function serveStatic(config: Config | UnauthorizedConfig, staticP
14
15
  * Serve single file (e.g., logo.png, icons/star.svg) with dual compatibility
15
16
  * @param config - App configuration
16
17
  * @param filePath - Path to file
18
+ * @param onMissing - Called instead of the default handling when the file does not exist
17
19
  */
18
- export declare function serveFile(config: Config | UnauthorizedConfig, filePath: string): (req: Request, res: Response, next: Function) => Promise<void>;
20
+ export declare function serveFile(config: Config | UnauthorizedConfig, filePath: string, onMissing?: MissingFileHandler): (req: Request, res: Response, next: Function) => Promise<void>;
21
+ export {};
@@ -23,7 +23,7 @@ const path_1 = __importDefault(require("path"));
23
23
  /**
24
24
  * Proxy Workers Assets response to Express response
25
25
  */
26
- function proxyAssetsResponse(fetcher, assetPath, baseUrl, req, res, next) {
26
+ function proxyAssetsResponse(fetcher, assetPath, baseUrl, req, res, next, onMissing) {
27
27
  return __awaiter(this, void 0, void 0, function* () {
28
28
  try {
29
29
  if (assetPath === '/') {
@@ -31,6 +31,9 @@ function proxyAssetsResponse(fetcher, assetPath, baseUrl, req, res, next) {
31
31
  }
32
32
  const assetUrl = new URL(assetPath, baseUrl);
33
33
  const response = yield fetcher.fetch(assetUrl.toString());
34
+ if (response.status === 404 && onMissing) {
35
+ return onMissing(req, res);
36
+ }
34
37
  if (!response.ok) {
35
38
  return next();
36
39
  }
@@ -68,19 +71,38 @@ function serveStatic(config, staticPath) {
68
71
  * Serve single file (e.g., logo.png, icons/star.svg) with dual compatibility
69
72
  * @param config - App configuration
70
73
  * @param filePath - Path to file
74
+ * @param onMissing - Called instead of the default handling when the file does not exist
71
75
  */
72
- function serveFile(config, filePath) {
76
+ function serveFile(config, filePath, onMissing) {
73
77
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
74
78
  var _a;
75
79
  if ((_a = config.assetsConfig) === null || _a === void 0 ? void 0 : _a.fetcher) {
76
80
  const assetsFetcher = config.assetsConfig.fetcher;
77
81
  const assetPath = filePath.startsWith('/') ? filePath : `/${filePath}`;
78
- yield proxyAssetsResponse(assetsFetcher, assetPath, config.baseUrl, req, res, next);
82
+ yield proxyAssetsResponse(assetsFetcher, assetPath, config.baseUrl, req, res, next, onMissing);
79
83
  }
80
84
  else {
81
85
  const rootDir = path_1.default.dirname(__dirname);
82
86
  const absolutePath = path_1.default.isAbsolute(filePath) ? filePath : path_1.default.join(rootDir, filePath);
83
- res.sendFile(absolutePath);
87
+ if (!onMissing) {
88
+ res.sendFile(absolutePath);
89
+ return;
90
+ }
91
+ // Mirrors Express default sendFile error handling, except for a missing file
92
+ res.sendFile(absolutePath, (err) => {
93
+ if (!err) {
94
+ return;
95
+ }
96
+ if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
97
+ return onMissing(req, res);
98
+ }
99
+ if (err.code === 'EISDIR') {
100
+ return next();
101
+ }
102
+ if (err.code !== 'ECONNABORTED' && err.syscall !== 'write') {
103
+ next(err);
104
+ }
105
+ });
84
106
  }
85
107
  });
86
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "3.2.2",
3
+ "version": "3.4.0",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -47,6 +47,7 @@
47
47
  "lint": "eslint --fix \"{src,tests,ui}/**/*.{js,ts,tsx}\"",
48
48
  "prettier": "prettier --config .prettierrc {src,ui}/**/*.{ts,tsx} --write",
49
49
  "lint-ci": "eslint \"{src,tests,ui}/**/*.{js,ts,tsx}\"",
50
+ "typecheck-tests": "tsc -p tsconfig.test.json",
50
51
  "test-coverage": "jest --ci --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html",
51
52
  "test": "jest",
52
53
  "copy-migrations": "cp -R src/storage/drizzle/sqlite/migrations out/storage/drizzle/sqlite/migrations && cp -R src/storage/drizzle/postgresql/migrations out/storage/drizzle/postgresql/migrations",