datagrok-tools 5.1.7 → 5.1.8

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.
@@ -15,6 +15,9 @@ const annotationForApiFile = `/**\nThis file is auto-generated by the grok api c
15
15
  const sep = '\n';
16
16
  const packageFuncDirs = ['package.ts', 'package.g.ts'];
17
17
  const apiFile = 'package-api.ts';
18
+ function normEol(s) {
19
+ return s.replace(/\r\n/g, '\n');
20
+ }
18
21
  const curDir = process.cwd();
19
22
  const srcDir = _path.default.join(curDir, 'src');
20
23
  const funcFilePath = _path.default.join(_fs.default.existsSync(srcDir) ? srcDir : curDir, apiFile);
@@ -107,11 +110,11 @@ function saveWrappersToFile(namespaceName, wrappers) {
107
110
  if (wrappers.length === 0) return;
108
111
  if (!_fs.default.existsSync(funcFilePath)) createApiFile();
109
112
  const scriptApi = new utils.TemplateBuilder(utils.namespaceTemplate).replace('PACKAGE_NAMESPACE', namespaceName).replace('NAME', wrappers.join(sep.repeat(2)));
110
- _fs.default.appendFileSync(funcFilePath, sep + scriptApi.build() + sep);
113
+ _fs.default.appendFileSync(funcFilePath, normEol(sep + scriptApi.build() + sep));
111
114
  color.log(`Successfully generated file ${apiFile}${sep}`, 'success');
112
115
  }
113
116
  function createApiFile() {
114
- _fs.default.writeFileSync(funcFilePath, annotationForApiFile + utils.dgImports + sep, 'utf8');
117
+ _fs.default.writeFileSync(funcFilePath, normEol(annotationForApiFile + utils.dgImports + sep), 'utf8');
115
118
  }
116
119
  function checkNameColision(name) {
117
120
  if (names.has(name)) console.log('There is collision in name ' + name);
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.applyFilter = applyFilter;
8
+ exports.build = build;
9
+ exports.confirm = confirm;
10
+ exports.discoverPackages = discoverPackages;
11
+ exports.getNestedValue = getNestedValue;
12
+ var _fs = _interopRequireDefault(require("fs"));
13
+ var _path = _interopRequireDefault(require("path"));
14
+ var _util = require("util");
15
+ var _child_process = require("child_process");
16
+ var readline = _interopRequireWildcard(require("readline"));
17
+ var utils = _interopRequireWildcard(require("../utils/utils"));
18
+ var color = _interopRequireWildcard(require("../utils/color-utils"));
19
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
20
+ const execAsync = (0, _util.promisify)(_child_process.exec);
21
+ async function build(args) {
22
+ if (args.verbose) color.setVerbose(true);
23
+ const buildCmd = args['no-incremental'] ? 'npm run build' : 'npm run build -- --env incremental';
24
+ if (args.recursive) return await buildRecursive(process.cwd(), args, buildCmd);else return await buildSingle(process.cwd(), buildCmd);
25
+ }
26
+ async function buildSingle(dir, buildCmd) {
27
+ if (!utils.isPackageDir(dir)) {
28
+ color.error('Not a package directory (no package.json found)');
29
+ return false;
30
+ }
31
+ const packageJson = JSON.parse(_fs.default.readFileSync(_path.default.join(dir, 'package.json'), 'utf-8'));
32
+ const name = packageJson.friendlyName || packageJson.name;
33
+ console.log(`Building ${name}...`);
34
+ try {
35
+ await utils.runScript('npm install', dir, color.isVerbose());
36
+ await utils.runScript(buildCmd, dir, color.isVerbose());
37
+ color.success(`Successfully built ${name}`);
38
+ return true;
39
+ } catch (error) {
40
+ color.error(`Failed to build ${name}`);
41
+ if (error.message) color.error(error.message);
42
+ return false;
43
+ }
44
+ }
45
+ async function buildRecursive(baseDir, args, buildCmd) {
46
+ const packages = discoverPackages(baseDir);
47
+ if (packages.length === 0) {
48
+ color.warn('No packages found in the current directory');
49
+ return false;
50
+ }
51
+ const filtered = args.filter ? applyFilter(packages, args.filter) : packages;
52
+ if (filtered.length === 0) {
53
+ color.warn('No packages match the filter');
54
+ return false;
55
+ }
56
+ console.log(`Found ${filtered.length} package(s): ${filtered.map(p => p.friendlyName).join(', ')}`);
57
+ if (!args.silent) {
58
+ const confirmed = await confirm(`\nBuild ${filtered.length} package(s)?`);
59
+ if (!confirmed) {
60
+ console.log('Aborted.');
61
+ return false;
62
+ }
63
+ }
64
+ const maxParallel = args.parallel || 4;
65
+ const results = await buildParallel(filtered, buildCmd, maxParallel);
66
+ return results.every(r => r.success);
67
+ }
68
+ function discoverPackages(baseDir) {
69
+ const entries = _fs.default.readdirSync(baseDir);
70
+ const packages = [];
71
+ for (const entry of entries) {
72
+ if (entry.startsWith('.') || entry === 'node_modules') continue;
73
+ const dir = _path.default.join(baseDir, entry);
74
+ try {
75
+ if (!_fs.default.statSync(dir).isDirectory()) continue;
76
+ } catch (_) {
77
+ continue;
78
+ }
79
+ const packageJsonPath = _path.default.join(dir, 'package.json');
80
+ if (!_fs.default.existsSync(packageJsonPath)) continue;
81
+ try {
82
+ const packageJson = JSON.parse(_fs.default.readFileSync(packageJsonPath, 'utf-8'));
83
+ packages.push({
84
+ dir,
85
+ name: packageJson.name || entry,
86
+ friendlyName: packageJson.friendlyName || packageJson.name || entry,
87
+ version: packageJson.version || '0.0.0',
88
+ packageJson
89
+ });
90
+ } catch (_) {
91
+ continue;
92
+ }
93
+ }
94
+ return packages.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
95
+ }
96
+ function applyFilter(packages, filterStr) {
97
+ const conditions = filterStr.split('&&').map(s => s.trim());
98
+ const parsedConditions = conditions.map(cond => {
99
+ const colonIdx = cond.indexOf(':');
100
+ if (colonIdx === -1) return {
101
+ field: cond,
102
+ pattern: new RegExp('.')
103
+ };
104
+ const field = cond.substring(0, colonIdx).trim();
105
+ const pattern = new RegExp(cond.substring(colonIdx + 1).trim());
106
+ return {
107
+ field,
108
+ pattern
109
+ };
110
+ });
111
+ return packages.filter(pkg => {
112
+ for (const cond of parsedConditions) {
113
+ const value = getNestedValue(pkg.packageJson, cond.field);
114
+ if (value === undefined || !cond.pattern.test(String(value))) return false;
115
+ }
116
+ return true;
117
+ });
118
+ }
119
+ function getNestedValue(obj, path) {
120
+ const parts = path.split('.');
121
+ let current = obj;
122
+ for (const part of parts) {
123
+ if (current == null || typeof current !== 'object') return undefined;
124
+ current = current[part];
125
+ }
126
+ return current;
127
+ }
128
+ async function buildParallel(packages, buildCmd, maxParallel) {
129
+ const results = [];
130
+ const headers = ['Plugin', 'Version', 'Build time', 'Bundle size'];
131
+ const widths = [Math.max(headers[0].length, ...packages.map(p => p.friendlyName.length)), Math.max(headers[1].length, ...packages.map(p => p.version.length)), Math.max(headers[2].length, 10), Math.max(headers[3].length, 40)];
132
+ const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
133
+ console.log(`\nBuilding with ${maxParallel} parallel job(s)...`);
134
+ console.log(headers.map((h, i) => pad(h, widths[i])).join(' | '));
135
+ console.log(widths.map(w => '-'.repeat(w)).join('-+-'));
136
+ const buildOne = async pkg => {
137
+ const start = Date.now();
138
+ let success = true;
139
+ let buildTime = '';
140
+ let bundleSize = '';
141
+ try {
142
+ await execAsync('npm install', {
143
+ cwd: pkg.dir,
144
+ maxBuffer: 10 * 1024 * 1024
145
+ });
146
+ await execAsync(buildCmd, {
147
+ cwd: pkg.dir,
148
+ maxBuffer: 10 * 1024 * 1024
149
+ });
150
+ const elapsed = (Date.now() - start) / 1000;
151
+ buildTime = `${elapsed.toFixed(1)}s`;
152
+ bundleSize = getBundleSize(pkg.dir);
153
+ } catch (error) {
154
+ success = false;
155
+ buildTime = 'Error';
156
+ const raw = (error.stderr || error.stdout || error.message || 'Unknown error').trim();
157
+ bundleSize = raw.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').substring(0, 40);
158
+ }
159
+ const result = {
160
+ name: pkg.friendlyName,
161
+ version: pkg.version,
162
+ buildTime,
163
+ bundleSize,
164
+ success
165
+ };
166
+ results.push(result);
167
+ const cells = [result.name, result.version, result.buildTime, result.bundleSize];
168
+ const line = cells.map((cell, j) => pad(cell, widths[j])).join(' | ');
169
+ if (success) color.info(line);else color.error(line);
170
+ return result;
171
+ };
172
+ let idx = 0;
173
+ const next = async () => {
174
+ while (idx < packages.length) {
175
+ const pkg = packages[idx++];
176
+ await buildOne(pkg);
177
+ }
178
+ };
179
+ const workers = Array.from({
180
+ length: Math.min(maxParallel, packages.length)
181
+ }, () => next());
182
+ await Promise.all(workers);
183
+ const succeeded = results.filter(r => r.success).length;
184
+ const failed = results.length - succeeded;
185
+ console.log('');
186
+ if (failed === 0) color.success(`All ${results.length} package(s) built successfully`);else color.warn(`${succeeded} succeeded, ${failed} failed`);
187
+ return results;
188
+ }
189
+ function getBundleSize(dir) {
190
+ const bundlePath = _path.default.join(dir, 'dist', 'package.js');
191
+ try {
192
+ const stats = _fs.default.statSync(bundlePath);
193
+ return `${(stats.size / 1024).toFixed(1)} KB`;
194
+ } catch (_) {
195
+ return 'N/A';
196
+ }
197
+ }
198
+ function confirm(message) {
199
+ const rl = readline.createInterface({
200
+ input: process.stdin,
201
+ output: process.stdout
202
+ });
203
+ return new Promise(resolve => {
204
+ rl.question(`${message} [Y/n] `, answer => {
205
+ rl.close();
206
+ resolve(answer.trim().toLowerCase() !== 'n');
207
+ });
208
+ });
209
+ }
@@ -8,6 +8,7 @@ exports.check = check;
8
8
  exports.checkChangelog = checkChangelog;
9
9
  exports.checkDatagrokApiImports = checkDatagrokApiImports;
10
10
  exports.checkFuncSignatures = checkFuncSignatures;
11
+ exports.checkHeavyImports = checkHeavyImports;
11
12
  exports.checkImportStatements = checkImportStatements;
12
13
  exports.checkNpmIgnore = checkNpmIgnore;
13
14
  exports.checkPackageFile = checkPackageFile;
@@ -23,6 +24,30 @@ var _const = require("../utils/const");
23
24
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
24
25
  const warns = ['Latest package version', 'Datagrok API version should contain'];
25
26
  const forbiddenNames = ['function', 'class', 'export'];
27
+ /** Static imports of these libraries significantly increase the main bundle size. */
28
+ const HEAVY_IMPORT_RULES = [
29
+ // Test utilities — should never reach production bundles
30
+ {
31
+ pattern: '@datagrok-libraries/test',
32
+ reason: 'test-only library imported in production code',
33
+ suggestion: 'replace delay() with DG.delay(), testEvent() with alternatives from utils'
34
+ },
35
+ // Heavy UI/editor libraries — only flagged when not the package\'s core purpose
36
+ {
37
+ pattern: 'codemirror',
38
+ reason: 'heavy text editor (~870 KB) statically bundled',
39
+ suggestion: 'use dynamic import() inside the function that opens the editor'
40
+ }, {
41
+ pattern: 'konva',
42
+ reason: 'heavy canvas library (~410 KB) statically bundled',
43
+ suggestion: 'use dynamic import() to load on demand'
44
+ },
45
+ // Heavy data-processing libraries
46
+ {
47
+ pattern: 'exceljs',
48
+ reason: 'heavy Excel library (~940 KB) statically bundled',
49
+ suggestion: 'offload to a web worker (see PowerPack/src/workers/exceljs-worker.ts)'
50
+ }];
26
51
  const namesInFiles = new Map();
27
52
  function check(args) {
28
53
  if (args.verbose !== undefined || args.v !== undefined) color.setVerbose(args.verbose || args.v || false);
@@ -41,9 +66,9 @@ function runChecks(packagePath, soft = false, noExit = false) {
41
66
  path: packagePath,
42
67
  ignoreFiles: ['.npmignore', '.gitignore']
43
68
  }).filter(e => !e.includes('node_modules'));
44
- const jsTsFiles = files.filter(f => (f.startsWith('src' + _path.default.sep) || f.startsWith('queries' + _path.default.sep) || f.startsWith('scripts' + _path.default.sep)) && (f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.sql') || f.endsWith('.py')));
69
+ const jsTsFiles = files.filter(f => (f.startsWith('src/') || f.startsWith('queries/') || f.startsWith('scripts/')) && (f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.sql') || f.endsWith('.py')));
45
70
  const packageFiles = ['src/package.ts', 'src/detectors.ts', 'src/package.js', 'src/detectors.js', 'src/package-test.ts', 'src/package-test.js', 'package.js', 'detectors.js'];
46
- // const funcFiles = jsTsFiles.filter((f) => packageFiles.includes(f));
71
+ // const funcFiles = jsTsFiles.filter((f) => packageFiles.includes(f));
47
72
  const errors = [];
48
73
  const warnings = [];
49
74
  const packageFilePath = _path.default.join(packagePath, 'package.json');
@@ -67,6 +92,7 @@ function runChecks(packagePath, soft = false, noExit = false) {
67
92
  if (externals) errors.push(...checkImportStatements(packagePath, jsTsFiles, externals));
68
93
  }
69
94
  errors.push(...checkDatagrokApiImports(packagePath, jsTsFiles));
95
+ warnings.push(...checkHeavyImports(packagePath, jsTsFiles, _path.default.basename(packagePath)));
70
96
  if (!soft) errors.push(...checkSourceMap(packagePath));
71
97
  errors.push(...checkNpmIgnore(packagePath));
72
98
  warnings.push(...checkScriptNames(packagePath));
@@ -188,6 +214,31 @@ function checkDatagrokApiImports(packagePath, files) {
188
214
  }
189
215
  return errors;
190
216
  }
217
+ function checkHeavyImports(packagePath, files, packageName = '') {
218
+ const warnings = [];
219
+ const isTestPackage = /tests?$/i.test(packageName);
220
+ const staticImportRegex = /^\s*import\s+(?!type\s).*['"]([^'"]+)['"]/;
221
+ for (const file of files) {
222
+ const isTestFile = file.includes('/tests/') || file.includes('/test/') || _path.default.basename(file).includes('-test.') || _path.default.basename(file).includes('-testing.') || _path.default.basename(file) === 'package-test.ts' || _path.default.basename(file) === 'package-test.js';
223
+ const isWorkerFile = file.includes('/workers/');
224
+ const isVendoredLib = file.includes('/libs/');
225
+ if (isWorkerFile || isVendoredLib) continue;
226
+ const content = _fs.default.readFileSync(_path.default.join(packagePath, file), {
227
+ encoding: 'utf-8'
228
+ });
229
+ for (const line of content.split('\n')) {
230
+ const m = line.match(staticImportRegex);
231
+ if (!m) continue;
232
+ const specifier = m[1];
233
+ for (const rule of HEAVY_IMPORT_RULES) {
234
+ if (!specifier.includes(rule.pattern)) continue;
235
+ if (isTestFile || isTestPackage) continue;
236
+ warnings.push(`File "${file}": ${rule.reason}.\n` + ` Found: ${line.trim()}\n` + ` Hint: ${rule.suggestion}\n`);
237
+ }
238
+ }
239
+ }
240
+ return warnings;
241
+ }
191
242
  const TYPE_ALIASES = {
192
243
  file: ['fileinfo'],
193
244
  dynamic: ['searchprovider']
@@ -12,6 +12,7 @@ Datagrok's package management tool
12
12
  Commands:
13
13
  add Add an object template
14
14
  api Create wrapper functions
15
+ build Build a package or multiple packages
15
16
  check Check package content (function signatures, etc.)
16
17
  config Create and manage config files
17
18
  create Create a package
@@ -144,15 +145,15 @@ const HELP_TEST = `
144
145
  Usage: grok test
145
146
 
146
147
  Options:
147
- [--package] [--category] [--test] [--host] [--csv] [--gui] [--skip-build] [--skip-publish] [--link] [--catchUnhandled] [--report] [--record] [--verbose] [--platform] [--benchmark] [--stress-test] [--debug] [--all]
148
+ [--package] [--category] [--test] [--host] [--csv] [--gui] [--skip-build] [--skip-publish] [--link] [--catchUnhandled] [--report] [--record] [--verbose] [--platform] [--benchmark] [--stress-test] [--debug] [--all] [-r | --recursive] [--filter] [--parallel N]
148
149
 
149
150
  --package Specify a package name to run tests for
150
151
  --category Specify a category name to run tests for
151
- --test Specify a test name to run
152
+ --test Specify a test name to run
152
153
  --host Host alias as in the config file
153
154
  --csv Save the test report in a CSV file
154
155
  --gui Launch graphical interface (non-headless mode)
155
- --debug Enables debug point on tests run (useless without gui mode)
156
+ --debug Enables debug point on tests run (useless without gui mode)
156
157
  --verbose Show debug information
157
158
  --retry --no-retry Enables or disables browser reload after a failed test
158
159
  --report Report failed tests to audit, notifies package author (default=false)
@@ -165,9 +166,18 @@ Options:
165
166
  --benchmark Runs tests in benchmark mode
166
167
  --stress-test Runs shuffled stress-test only
167
168
  --all Runs tests for all available packages(run in packages directory)
168
-
169
+ --recursive Test all packages in the current directory (parallel, table output)
170
+ --filter Filter packages by package.json fields (e.g. --filter "category:Cheminformatics")
171
+ --parallel N Max parallel test jobs (default: 4)
172
+
169
173
  Run package tests
170
174
 
175
+ Examples:
176
+ grok test -r Test all packages
177
+ grok test -r --filter "category:Cheminformatics" Test matching packages
178
+ grok test -r --parallel 2 --host dev Test with 2 jobs against dev
179
+ grok test -r --skip-build --skip-publish Test without rebuilding
180
+
171
181
  See instructions:
172
182
  https://datagrok.ai/help/develop/how-to/test-packages#local-testing
173
183
  `;
@@ -227,6 +237,28 @@ Example:
227
237
 
228
238
  meta: { role: 'viewer,ml' }
229
239
  `;
240
+ const HELP_BUILD = `
241
+ Usage: grok build
242
+
243
+ Build a package in the current directory, or recursively build multiple packages.
244
+
245
+ Options:
246
+ [-r | --recursive] [-s | --silent] [--filter] [--no-incremental] [--parallel N] [-v | --verbose]
247
+
248
+ --recursive Build all packages in the current directory
249
+ --silent Skip confirmation prompt (for recursive builds)
250
+ --filter Filter packages by package.json fields (e.g. --filter "category:Cheminformatics")
251
+ --no-incremental Run a full build instead of the default incremental build
252
+ --parallel N Max parallel builds (default: 4)
253
+ --verbose Print detailed output
254
+
255
+ Examples:
256
+ grok build Build the current package
257
+ grok build -r Build all packages in the current directory
258
+ grok build -r -s Build all packages without confirmation
259
+ grok build -r --filter "category:Cheminformatics" Build only matching packages
260
+ grok build -r --parallel 8 Build with 8 parallel jobs
261
+ `;
230
262
 
231
263
  // const HELP_MIGRATE = `
232
264
  // Usage: grok migrate
@@ -238,6 +270,7 @@ Example:
238
270
  const help = exports.help = {
239
271
  add: HELP_ADD,
240
272
  api: HELP_API,
273
+ build: HELP_BUILD,
241
274
  check: HELP_CHECK,
242
275
  config: HELP_CONFIG,
243
276
  create: HELP_CREATE,
@@ -6,18 +6,22 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.test = test;
8
8
  var _child_process = require("child_process");
9
+ var _util = require("util");
9
10
  var _fs = _interopRequireDefault(require("fs"));
10
11
  var _os = _interopRequireDefault(require("os"));
11
12
  var _path = _interopRequireDefault(require("path"));
12
13
  var _jsYaml = _interopRequireDefault(require("js-yaml"));
13
14
  var utils = _interopRequireWildcard(require("../utils/utils"));
14
15
  var color = _interopRequireWildcard(require("../utils/color-utils"));
16
+ var _build = require("./build");
15
17
  var Papa = _interopRequireWildcard(require("papaparse"));
16
18
  var _testUtils = _interopRequireWildcard(require("../utils/test-utils"));
17
19
  var testUtils = _testUtils;
18
20
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
19
21
  /* eslint-disable max-len */
20
22
 
23
+ const execAsync = (0, _util.promisify)(_child_process.exec);
24
+ const execFileAsync = (0, _util.promisify)(_child_process.execFile);
21
25
  const testInvocationTimeout = 3600000;
22
26
  const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry'];
23
27
  const curDir = process.cwd();
@@ -53,6 +57,7 @@ function findGitRoot(startDir) {
53
57
  }
54
58
  }
55
59
  async function test(args) {
60
+ if (args.recursive) return await testRecursive(process.cwd(), args);
56
61
  const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
57
62
  encoding: 'utf-8'
58
63
  }));
@@ -312,4 +317,129 @@ function readCSVResultData(data) {
312
317
  headers: parsed.meta.fields || [],
313
318
  rows: parsed.data
314
319
  };
320
+ }
321
+ async function testRecursive(baseDir, args) {
322
+ const packages = (0, _build.discoverPackages)(baseDir);
323
+ if (packages.length === 0) {
324
+ color.warn('No packages found in the current directory');
325
+ return false;
326
+ }
327
+ const filtered = args.filter ? (0, _build.applyFilter)(packages, args.filter) : packages;
328
+ if (filtered.length === 0) {
329
+ color.warn('No packages match the filter');
330
+ return false;
331
+ }
332
+ console.log(`Found ${filtered.length} package(s): ${filtered.map(p => p.friendlyName).join(', ')}`);
333
+ const confirmed = await (0, _build.confirm)(`\nTest ${filtered.length} package(s)?`);
334
+ if (!confirmed) {
335
+ console.log('Aborted.');
336
+ return false;
337
+ }
338
+ const maxParallel = args.parallel || 4;
339
+ const results = await testParallel(filtered, args, maxParallel);
340
+ return results.every(r => r.success);
341
+ }
342
+ function buildTestArgs(args) {
343
+ const grokScript = _path.default.resolve(__dirname, '..', 'grok.js');
344
+ const parts = [grokScript, 'test'];
345
+ if (args.host) parts.push(`--host=${args.host}`);
346
+ if (args['skip-build']) parts.push('--skip-build');
347
+ if (args['skip-publish']) parts.push('--skip-publish');
348
+ if (args.link) parts.push('--link');
349
+ if (args.verbose) parts.push('--verbose');
350
+ if (args.csv) parts.push('--csv');
351
+ if (args.catchUnhandled) parts.push('--catchUnhandled');
352
+ if (args.report) parts.push('--report');
353
+ if (args.benchmark) parts.push('--benchmark');
354
+ if (args['stress-test']) parts.push('--stress-test');
355
+ if (args['ci-cd']) parts.push('--ci-cd');
356
+ if (args['no-retry']) parts.push('--no-retry');
357
+ return parts;
358
+ }
359
+ function parseTestOutput(stdout) {
360
+ const passedMatch = stdout.match(/Passed amount:\s*(\d+)/);
361
+ const failedMatch = stdout.match(/Failed amount:\s*(\d+)/);
362
+ const skippedMatch = stdout.match(/Skipped amount:\s*(\d+)/);
363
+ if (!passedMatch && !failedMatch) return null;
364
+ return {
365
+ passed: passedMatch ? parseInt(passedMatch[1]) : 0,
366
+ failed: failedMatch ? parseInt(failedMatch[1]) : 0,
367
+ skipped: skippedMatch ? parseInt(skippedMatch[1]) : 0
368
+ };
369
+ }
370
+ async function testParallel(packages, args, maxParallel) {
371
+ const results = [];
372
+ const cmdArgs = buildTestArgs(args);
373
+ const headers = ['Plugin', 'Version', 'Tests', 'Time'];
374
+ const widths = [Math.max(headers[0].length, ...packages.map(p => p.friendlyName.length)), Math.max(headers[1].length, ...packages.map(p => p.version.length)), Math.max(headers[2].length, 18), Math.max(headers[3].length, 8)];
375
+ const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
376
+ console.log(`\nTesting with ${maxParallel} parallel job(s)...`);
377
+ console.log(headers.map((h, i) => pad(h, widths[i])).join(' | '));
378
+ console.log(widths.map(w => '-'.repeat(w)).join('-+-'));
379
+ const testOne = async pkg => {
380
+ const start = Date.now();
381
+ let success = true;
382
+ let tests = '';
383
+ let time = '';
384
+ try {
385
+ const {
386
+ stdout
387
+ } = await execFileAsync(process.execPath, cmdArgs, {
388
+ cwd: pkg.dir,
389
+ maxBuffer: 50 * 1024 * 1024,
390
+ timeout: testInvocationTimeout
391
+ });
392
+ const elapsed = (Date.now() - start) / 1000;
393
+ time = `${elapsed.toFixed(1)}s`;
394
+ const counts = parseTestOutput(stdout);
395
+ if (counts) {
396
+ const total = counts.passed + counts.failed + counts.skipped;
397
+ tests = `${counts.passed} of ${total} passed`;
398
+ success = counts.failed === 0;
399
+ } else {
400
+ tests = 'Done (no counts)';
401
+ }
402
+ } catch (error) {
403
+ success = false;
404
+ const elapsed = (Date.now() - start) / 1000;
405
+ time = `${elapsed.toFixed(1)}s`;
406
+ const stdout = error.stdout || '';
407
+ const counts = parseTestOutput(stdout);
408
+ if (counts) {
409
+ const total = counts.passed + counts.failed + counts.skipped;
410
+ tests = `${counts.passed} of ${total} passed`;
411
+ } else {
412
+ const raw = (error.stderr || error.stdout || error.message || 'Unknown error').trim();
413
+ tests = raw.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').substring(0, 30);
414
+ }
415
+ }
416
+ const result = {
417
+ name: pkg.friendlyName,
418
+ version: pkg.version,
419
+ tests,
420
+ time,
421
+ success
422
+ };
423
+ results.push(result);
424
+ const cells = [result.name, result.version, result.tests, result.time];
425
+ const line = cells.map((cell, j) => pad(cell, widths[j])).join(' | ');
426
+ if (success) color.info(line);else color.error(line);
427
+ return result;
428
+ };
429
+ let idx = 0;
430
+ const next = async () => {
431
+ while (idx < packages.length) {
432
+ const pkg = packages[idx++];
433
+ await testOne(pkg);
434
+ }
435
+ };
436
+ const workers = Array.from({
437
+ length: Math.min(maxParallel, packages.length)
438
+ }, () => next());
439
+ await Promise.all(workers);
440
+ const succeeded = results.filter(r => r.success).length;
441
+ const failed = results.length - succeeded;
442
+ console.log('');
443
+ if (failed === 0) color.success(`All ${results.length} package(s) passed`);else color.warn(`${succeeded} passed, ${failed} failed`);
444
+ return results;
315
445
  }
package/bin/grok.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const argv = require('minimist')(process.argv.slice(2), {
3
- alias: {k: 'key', h: 'help', r: 'recursive'},
3
+ alias: {k: 'key', h: 'help', r: 'recursive', s: 'silent'},
4
4
  });
5
5
  const help = require('./commands/help').help;
6
6
  const runAllCommand = require('./utils/utils').runAll;
@@ -8,6 +8,7 @@ const runAllCommand = require('./utils/utils').runAll;
8
8
  const commands = {
9
9
  add: require('./commands/add').add,
10
10
  api: require('./commands/api').api,
11
+ build: require('./commands/build').build,
11
12
  check: require('./commands/check').check,
12
13
  config: require('./commands/config').config,
13
14
  create: require('./commands/create').create,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "5.1.7",
3
+ "version": "5.1.8",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -20,6 +20,10 @@ const {toCamelCase} = require('../bin/commands/migrate');
20
20
  const {api} = require('../bin/commands/api');
21
21
 
22
22
  const baseImport = 'import * as DG from \'datagrok-api/dg\';\n';
23
+
24
+ function normEol(s) {
25
+ return s.replace(/\r\n/g, '\n');
26
+ }
23
27
  // eslint-disable-next-line max-len
24
28
  const annotationForGeneratedFile = `/**\nThis file is auto-generated by the webpack command.\nIf you notice any changes, please push them to the repository.\nDo not edit this file manually.\n*/\n`;
25
29
 
@@ -85,7 +89,7 @@ class FuncGeneratorPlugin {
85
89
  }
86
90
  });
87
91
  this._insertImports([...imports]);
88
- fs.appendFileSync(this.options.outputPath, functions.join(''), 'utf-8');
92
+ fs.appendFileSync(this.options.outputPath, normEol(functions.join('')), 'utf-8');
89
93
  }
90
94
  this._checkPackageFileForDecoratorsExport(packageFilePath);
91
95
  // Uncommment to add obvious import/export
@@ -512,7 +516,7 @@ class FuncGeneratorPlugin {
512
516
  }
513
517
 
514
518
  _clearGeneratedFile() {
515
- fs.writeFileSync(this.options.outputPath, baseImport);
519
+ fs.writeFileSync(this.options.outputPath, normEol(baseImport));
516
520
  }
517
521
 
518
522
  _checkPackageFileForDecoratorsExport(packagePath) {
@@ -543,11 +547,11 @@ class FuncGeneratorPlugin {
543
547
  const content = fs.readFileSync(this.options.outputPath, 'utf-8');
544
548
  if (content) importArray.push(content);
545
549
  const output = importArray.join('');
546
- fs.writeFileSync(this.options.outputPath, `${output}`, 'utf-8');
550
+ fs.writeFileSync(this.options.outputPath, normEol(output), 'utf-8');
547
551
  } else {
548
552
  fs.writeFileSync(
549
553
  this.options.outputPath,
550
- `${annotationForGeneratedFile}${baseImport}\n${importArray.join('')}`,
554
+ normEol(`${annotationForGeneratedFile}${baseImport}\n${importArray.join('')}`),
551
555
  'utf-8',
552
556
  );
553
557
  }