package.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. import * as child_process from 'child_process';
  2. import * as crypto from 'crypto';
  3. import * as fs from 'fs/promises';
  4. import * as os from 'os';
  5. import * as path from 'path';
  6. import * as url from 'url';
  7. const args = process.argv.slice(2);
  8. const read = (match) => {
  9. if (args.length > 0 && (!match || args[0] === match)) {
  10. return args.shift();
  11. }
  12. return null;
  13. };
  14. let configuration = null;
  15. const dirname = (...args) => {
  16. const file = url.fileURLToPath(import.meta.url);
  17. const dir = path.dirname(file);
  18. return path.join(dir, ...args);
  19. };
  20. const load = async () => {
  21. const file = dirname('package.json');
  22. const content = await fs.readFile(file, 'utf-8');
  23. configuration = JSON.parse(content);
  24. };
  25. const clearLine = () => {
  26. if (process.stdout.clearLine) {
  27. process.stdout.clearLine();
  28. }
  29. };
  30. const write = (message) => {
  31. if (process.stdout.write) {
  32. process.stdout.write(message);
  33. }
  34. };
  35. const writeLine = (message) => {
  36. write(message + os.EOL);
  37. };
  38. const access = async (path) => {
  39. try {
  40. await fs.access(path);
  41. return true;
  42. } catch {
  43. return false;
  44. }
  45. };
  46. const rm = async (...args) => {
  47. const dir = dirname(...args);
  48. const exists = await access(dir);
  49. if (exists) {
  50. const paths = path.join(...args);
  51. writeLine(`rm ${paths}`);
  52. const options = { recursive: true, force: true };
  53. await fs.rm(dir, options);
  54. }
  55. };
  56. const mkdir = async (...args) => {
  57. const dir = dirname(...args);
  58. const exists = await access(dir);
  59. if (!exists) {
  60. const paths = path.join(...args);
  61. writeLine(`mkdir ${paths}`);
  62. const options = { recursive: true };
  63. await fs.mkdir(dir, options);
  64. }
  65. return dir;
  66. };
  67. const copy = async (source, target, filter) => {
  68. let files = await fs.readdir(source);
  69. files = filter ? files.filter((file) => filter(file)) : files;
  70. const promises = files.map((file) => fs.copyFile(path.join(source, file), path.join(target, file)));
  71. await Promise.all(promises);
  72. };
  73. const unlink = async (dir, filter) => {
  74. let files = await fs.readdir(dir);
  75. files = filter ? files.filter((file) => filter(file)) : files;
  76. const promises = files.map((file) => fs.unlink(path.join(dir, file)));
  77. await Promise.all(promises);
  78. };
  79. const exec = async (command, encoding, cwd) => {
  80. cwd = cwd || dirname();
  81. if (encoding) {
  82. return child_process.spawnSync(command, { shell: true, cwd, encoding });
  83. }
  84. child_process.execSync(command, { cwd, stdio: [0,1,2] });
  85. return '';
  86. };
  87. const sleep = (delay) => {
  88. return new Promise((resolve) => {
  89. setTimeout(resolve, delay);
  90. });
  91. };
  92. const request = async (url, init, status) => {
  93. const response = await fetch(url, init);
  94. if (status !== false && !response.ok) {
  95. throw new Error(`${response.status.toString()} ${response.statusText}`);
  96. }
  97. if (response.body) {
  98. const reader = response.body.getReader();
  99. let position = 0;
  100. const stream = new ReadableStream({
  101. start(controller) {
  102. const read = async () => {
  103. try {
  104. const result = await reader.read();
  105. if (result.done) {
  106. clearLine();
  107. controller.close();
  108. } else {
  109. position += result.value.length;
  110. write(` ${position} bytes\r`);
  111. controller.enqueue(result.value);
  112. read();
  113. }
  114. } catch (error) {
  115. controller.error(error);
  116. }
  117. };
  118. read();
  119. }
  120. });
  121. return new Response(stream, {
  122. status: response.status,
  123. statusText: response.statusText,
  124. headers: response.headers
  125. });
  126. }
  127. return response;
  128. };
  129. const download = async (url) => {
  130. writeLine(`download ${url}`);
  131. const response = await request(url);
  132. return response.arrayBuffer().then((buffer) => new Uint8Array(buffer));
  133. };
  134. const hash = async (url, algorithm) => {
  135. const data = await download(url);
  136. const hash = crypto.createHash(algorithm);
  137. hash.update(data);
  138. return hash.digest('hex');
  139. };
  140. const fork = async (organization, repository) => {
  141. const headers = {
  142. Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
  143. };
  144. writeLine(`github delete ${repository}`);
  145. await request(`https://api.github.com/repos/${process.env.GITHUB_USER}/${repository}`, {
  146. method: 'DELETE',
  147. headers
  148. }, false);
  149. await sleep(4000);
  150. writeLine(`github fork ${repository}`);
  151. await request(`https://api.github.com/repos/${organization}/${repository}/forks`, {
  152. method: 'POST',
  153. headers,
  154. body: ''
  155. });
  156. await sleep(4000);
  157. await rm('dist', repository);
  158. writeLine(`github clone ${repository}`);
  159. await exec(`git clone --depth=2 https://x-access-token:${process.env.GITHUB_TOKEN}@github.com/${process.env.GITHUB_USER}/${repository}.git dist/${repository}`);
  160. };
  161. const pullrequest = async (organization, repository, token, body) => {
  162. writeLine(`github push ${repository}`);
  163. await exec(`git -C dist/${repository} push`);
  164. writeLine(`github pullrequest ${repository}`);
  165. const headers = {
  166. Authorization: `Bearer ${token}`
  167. };
  168. await request(`https://api.github.com/repos/${organization}/${repository}/pulls`, {
  169. method: 'POST',
  170. headers,
  171. body: JSON.stringify(body)
  172. });
  173. };
  174. const clean = async () => {
  175. await rm('dist');
  176. await rm('node_modules');
  177. await rm('package-lock.json');
  178. await rm('yarn.lock');
  179. if (read('purge')) {
  180. await rm('third_party');
  181. }
  182. };
  183. const install = async () => {
  184. const node_modules = dirname('node_modules');
  185. let exists = await access(node_modules);
  186. if (exists) {
  187. const dependencies = { ...configuration.dependencies, ...configuration.devDependencies };
  188. const matches = await Promise.all(Object.entries(dependencies).map(async ([name, version]) => {
  189. const file = path.join('node_modules', name, 'package.json');
  190. const exists = await access(file);
  191. if (exists) {
  192. const content = await fs.readFile(file, 'utf8');
  193. const obj = JSON.parse(content);
  194. return obj.version === version;
  195. }
  196. return false;
  197. }));
  198. exists = matches.every((match) => match);
  199. if (!exists) {
  200. await clean();
  201. }
  202. }
  203. exists = await access(node_modules);
  204. if (!exists) {
  205. await exec('npm install');
  206. }
  207. try {
  208. await exec('python --version', 'utf-8');
  209. await exec('python -m pip install --upgrade --quiet setuptools ruff');
  210. } catch {
  211. // continue regardless of error
  212. }
  213. };
  214. const start = async () => {
  215. await install();
  216. await exec('npx electron .');
  217. };
  218. const build = async (target) => {
  219. switch (target || read()) {
  220. case 'web': {
  221. writeLine('build web');
  222. await rm('dist', 'web');
  223. await mkdir('dist', 'web');
  224. writeLine('cp source/dir dist/dir');
  225. const source_dir = dirname('source');
  226. const dist_dir = dirname('dist', 'web');
  227. const extensions = new Set(['html', 'css', 'js', 'json', 'ico', 'png']);
  228. await copy(source_dir, dist_dir, (file) => extensions.has(file.split('.').pop()));
  229. await rm('dist', 'web', 'app.js');
  230. await rm('dist', 'web', 'node.js');
  231. await rm('dist', 'web', 'desktop.mjs');
  232. const contentFile = dirname('dist', 'web', 'index.html');
  233. let content = await fs.readFile(contentFile, 'utf-8');
  234. content = content.replace(/(<meta\s*name="version"\s*content=")(.*)(">)/m, (match, p1, p2, p3) => {
  235. return p1 + configuration.version + p3;
  236. });
  237. content = content.replace(/(<meta\s*name="date"\s*content=")(.*)(">)/m, (match, p1, p2, p3) => {
  238. return p1 + configuration.date + p3;
  239. });
  240. await fs.writeFile(contentFile, content, 'utf-8');
  241. break;
  242. }
  243. case 'electron': {
  244. const key = read();
  245. const target = key ? `electron ${key}` : 'electron';
  246. writeLine(`build ${target}`);
  247. await install();
  248. await exec('npx electron-builder install-app-deps');
  249. const table = new Map([
  250. ['mac', 'npx electron-builder --mac --universal --publish never --config.mac.identity=null'],
  251. ['windows', 'npx electron-builder --win --x64 --arm64 --publish never --config.win.azureSignOptions='],
  252. ['linux', 'npx electron-builder --linux --publish never']
  253. ]);
  254. const targets = table.has(key) ? [table.get(key)] : Array.from(table.values());
  255. for (const target of targets) {
  256. /* eslint-disable no-await-in-loop */
  257. await exec(target);
  258. /* eslint-enable no-await-in-loop */
  259. }
  260. break;
  261. }
  262. case 'python': {
  263. writeLine('build python');
  264. await exec('python package.py build version');
  265. await exec('python -m pip install --user build wheel --quiet');
  266. await exec('python -m build --wheel --outdir dist/pypi dist/pypi');
  267. if (read('install')) {
  268. await exec('python -m pip install --force-reinstall dist/pypi/*.whl');
  269. }
  270. break;
  271. }
  272. default: {
  273. writeLine('build');
  274. await rm('dist');
  275. await install();
  276. await build('web');
  277. await build('electron');
  278. await build('python');
  279. break;
  280. }
  281. }
  282. };
  283. const publish = async (target) => {
  284. const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
  285. const GITHUB_USER = process.env.GITHUB_USER;
  286. switch (target || read()) {
  287. case 'web': {
  288. writeLine('publish web');
  289. await build('web');
  290. await rm('dist', 'gh-pages');
  291. const url = `https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_USER}/netron.git`;
  292. await exec(`git clone --depth=1 ${url} --branch gh-pages ./dist/gh-pages 2>&1 > /dev/null`);
  293. writeLine('cp dist/web dist/gh-pages');
  294. const source_dir = dirname('dist', 'web');
  295. const target_dir = dirname('dist', 'gh-pages');
  296. await unlink(target_dir, (file) => file !== '.git');
  297. await copy(source_dir, target_dir);
  298. await exec('git -C dist/gh-pages add --all');
  299. await exec('git -C dist/gh-pages commit --amend --no-edit');
  300. await exec('git -C dist/gh-pages push --force origin gh-pages');
  301. break;
  302. }
  303. case 'electron': {
  304. const key = read();
  305. const target = key ? ` ${key}` : '';
  306. writeLine(`publish electron ${target}`);
  307. await install();
  308. await exec('npx electron-builder install-app-deps');
  309. const table = new Map([
  310. ['mac', 'npx electron-builder --mac --universal --publish always'],
  311. ['windows', 'npx electron-builder --win --x64 --arm64 --publish always'],
  312. ['linux', 'npx electron-builder --linux --publish always']
  313. ]);
  314. const targets = table.has(key) ? [table.get(key)] : Array.from(table.values());
  315. for (const target of targets) {
  316. /* eslint-disable no-await-in-loop */
  317. await exec(target);
  318. /* eslint-enable no-await-in-loop */
  319. }
  320. break;
  321. }
  322. case 'python': {
  323. writeLine('publish python');
  324. await build('python');
  325. await exec('python -m pip install --user twine');
  326. await exec('python -m twine upload --non-interactive --skip-existing --verbose dist/pypi/*.whl');
  327. break;
  328. }
  329. case 'cask': {
  330. writeLine('publish cask');
  331. await fork('Homebrew', 'homebrew-cask');
  332. const repository = `https://github.com/${configuration.repository}`;
  333. const url = `${repository}/releases/download/v#{version}/${configuration.productName}-#{version}-mac.zip`;
  334. const sha256 = await hash(url.replace(/#{version}/g, configuration.version), 'sha256');
  335. writeLine('update manifest');
  336. const dir = await mkdir('dist', 'homebrew-cask', 'Casks', 'n');
  337. const file = path.join(dir, 'netron.rb');
  338. await fs.writeFile(file, [
  339. `cask "${configuration.name}" do`,
  340. ` version "${configuration.version}"`,
  341. ` sha256 "${sha256.toLowerCase()}"`,
  342. '',
  343. ` url "${url}"`,
  344. ` name "${configuration.productName}"`,
  345. ` desc "${configuration.description.replace('Visualizer', 'Visualiser')}"`,
  346. ` homepage "${repository}"`,
  347. '',
  348. ' auto_updates true',
  349. '',
  350. ` app "${configuration.productName}.app"`,
  351. '',
  352. ' zap trash: [',
  353. ` "~/Library/Application Support/${configuration.productName}",`,
  354. ` "~/Library/Preferences/${configuration.build.appId}.plist",`,
  355. ` "~/Library/Saved Application State/${configuration.build.appId}.savedState",`,
  356. ' ]',
  357. 'end',
  358. ''
  359. ].join('\n'));
  360. writeLine('git push homebrew-cask');
  361. await exec('git -C dist/homebrew-cask add --all');
  362. await exec(`git -C dist/homebrew-cask commit -m "${configuration.name} ${configuration.version}"`);
  363. await pullrequest('Homebrew', 'homebrew-cask', process.env.GITHUB_TOKEN, {
  364. title: `${configuration.name} ${configuration.version}`,
  365. body: 'Update version and sha256',
  366. head: `${process.env.GITHUB_USER}:master`,
  367. base: 'master'
  368. });
  369. await rm('dist', 'homebrew-cask');
  370. break;
  371. }
  372. case 'winget': {
  373. writeLine('publish winget');
  374. await fork('microsoft', 'winget-pkgs');
  375. const name = configuration.name;
  376. const version = configuration.version;
  377. const product = configuration.productName;
  378. const publisher = configuration.author.name;
  379. const identifier = `${publisher.replace(' ', '')}.${product}`;
  380. const copyright = `Copyright (c) ${publisher}`;
  381. const repository = `https://github.com/${configuration.repository}`;
  382. const url = `${repository}/releases/download/v${version}/${product}-Setup-${version}.exe`;
  383. const content = await fs.readFile(configuration.build.extends, 'utf-8');
  384. const builder = JSON.parse(content);
  385. const extensions = builder.fileAssociations.map((entry) => `- ${entry.ext}`).sort().join('\n');
  386. const sha256 = await hash(url, 'sha256');
  387. const paths = ['dist', 'winget-pkgs', 'manifests', publisher[0].toLowerCase(), publisher.replace(' ', ''), product, version];
  388. await mkdir(...paths);
  389. writeLine('update manifest');
  390. const manifestFile = dirname(...paths, identifier);
  391. await fs.writeFile(`${manifestFile}.yaml`, [
  392. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json',
  393. `PackageIdentifier: ${identifier}`,
  394. `PackageVersion: ${version}`,
  395. 'DefaultLocale: en-US',
  396. 'ManifestType: version',
  397. 'ManifestVersion: 1.6.0',
  398. ''
  399. ].join('\n'));
  400. await fs.writeFile(`${manifestFile}.installer.yaml`, [
  401. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json',
  402. `PackageIdentifier: ${identifier}`,
  403. `PackageVersion: ${version}`,
  404. 'Platform:',
  405. '- Windows.Desktop',
  406. 'InstallModes:',
  407. '- silent',
  408. '- silentWithProgress',
  409. 'Installers:',
  410. '- Architecture: x86',
  411. ' Scope: user',
  412. ' InstallerType: nullsoft',
  413. ` InstallerUrl: ${url}`,
  414. ` InstallerSha256: ${sha256.toUpperCase()}`,
  415. ' InstallerLocale: en-US',
  416. ' InstallerSwitches:',
  417. ' Custom: /NORESTART',
  418. ' UpgradeBehavior: install',
  419. '- Architecture: arm64',
  420. ' Scope: user',
  421. ' InstallerType: nullsoft',
  422. ` InstallerUrl: ${url}`,
  423. ` InstallerSha256: ${sha256.toUpperCase()}`,
  424. ' InstallerLocale: en-US',
  425. ' InstallerSwitches:',
  426. ' Custom: /NORESTART',
  427. ' UpgradeBehavior: install',
  428. 'FileExtensions:',
  429. extensions,
  430. 'ManifestType: installer',
  431. 'ManifestVersion: 1.6.0',
  432. ''
  433. ].join('\n'));
  434. await fs.writeFile(`${manifestFile}.locale.en-US.yaml`, [
  435. '# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json',
  436. `PackageIdentifier: ${identifier}`,
  437. `PackageVersion: ${version}`,
  438. `PackageName: ${product}`,
  439. 'PackageLocale: en-US',
  440. `PackageUrl: ${repository}`,
  441. `Publisher: ${publisher}`,
  442. `PublisherUrl: ${repository}`,
  443. `PublisherSupportUrl: ${repository}/issues`,
  444. `Author: ${publisher}`,
  445. `License: ${configuration.license}`,
  446. `Copyright: ${copyright}`,
  447. `CopyrightUrl: ${repository}/blob/main/LICENSE`,
  448. `ShortDescription: ${configuration.description}`,
  449. `Description: ${configuration.description}`,
  450. `Moniker: ${name}`,
  451. 'Tags:',
  452. '- machine-learning',
  453. '- deep-learning',
  454. '- neural-network',
  455. 'ManifestType: defaultLocale',
  456. 'ManifestVersion: 1.6.0',
  457. ''
  458. ].join('\n'));
  459. writeLine('git push winget-pkgs');
  460. await exec('git -C dist/winget-pkgs add --all');
  461. await exec(`git -C dist/winget-pkgs commit -m "Update ${configuration.name} to ${configuration.version}"`);
  462. await pullrequest('microsoft', 'winget-pkgs', process.env.WINGET_TOKEN, {
  463. title: `Update ${configuration.productName} to ${configuration.version}`,
  464. body: '',
  465. head: `${process.env.GITHUB_USER}:master`,
  466. base: 'master'
  467. });
  468. await rm('dist', 'winget-pkgs');
  469. break;
  470. }
  471. default: {
  472. writeLine('publish');
  473. await rm('dist');
  474. await install();
  475. await publish('web');
  476. await publish('electron');
  477. await publish('python');
  478. await publish('cask');
  479. await publish('winget');
  480. break;
  481. }
  482. }
  483. };
  484. const lint = async () => {
  485. await install();
  486. writeLine('eslint');
  487. await exec('npx eslint --cache --cache-location ./dist/lint/.eslintcache');
  488. writeLine('ruff');
  489. await exec('python -m ruff check . --quiet');
  490. };
  491. const test = async (target) => {
  492. let models = true;
  493. while (true) {
  494. /* eslint-disable no-await-in-loop */
  495. if (target === 'desktop' || read('desktop')) {
  496. target = null;
  497. models = false;
  498. await exec('npx playwright install --with-deps');
  499. const host = process.platform === 'linux' && (process.env.GITHUB_ACTIONS || process.env.CI) ? 'xvfb-run -a ' : '';
  500. await exec(`${host}npx playwright test --config=test/playwright.config.js --project=desktop`);
  501. continue;
  502. }
  503. if (target === 'browser' || read('browser')) {
  504. target = null;
  505. models = false;
  506. if (process.platform !== 'win32') {
  507. await exec('npx playwright install --with-deps');
  508. const headed = process.env.GITHUB_ACTIONS || process.env.CI ? '' : ' --headed';
  509. await exec(`npx playwright test --config=test/playwright.config.js --project=browser${headed}`);
  510. }
  511. continue;
  512. }
  513. break;
  514. /* eslint-enable no-await-in-loop */
  515. }
  516. if (models) {
  517. target = target || args.join(' ');
  518. await exec(`node test/models.js ${target}`);
  519. }
  520. };
  521. const validate = async () => {
  522. writeLine('lint');
  523. await lint();
  524. writeLine('test');
  525. await test('tag:validation');
  526. writeLine('test desktop');
  527. await test('desktop');
  528. writeLine('test browser');
  529. await test('browser');
  530. };
  531. const update = async () => {
  532. const filter = new Set(process.argv.length > 3 ? process.argv.slice(3) : []);
  533. if (filter.size === 0) {
  534. const output = await exec('npm outdated --json', 'utf-8');
  535. const entries = JSON.parse(output.stdout);
  536. const compare = (a, b) => {
  537. const regex = /^\d+\.\d+\.\d+$/;
  538. if (!regex.test(a)) {
  539. throw new Error(`Invalid version format '${a}'.`);
  540. }
  541. if (!regex.test(b)) {
  542. throw new Error(`Invalid version format '${b}'.`);
  543. }
  544. a = a.split('.').map((v) => Number(v));
  545. b = b.split('.').map((v) => Number(v));
  546. if (a.length !== b.length) {
  547. throw new Error(`Invalid version a=${a.join('.')} b=${b.join('.')}`);
  548. }
  549. for (let i = 0; i < 3; i++) {
  550. if ((a[i] || 0) > (b[i] || 0)) {
  551. return 1;
  552. }
  553. if ((a[i] || 0) < (b[i] || 0)) {
  554. return -1;
  555. }
  556. }
  557. return 0;
  558. };
  559. for (const [name, entry] of Object.entries(entries)) {
  560. if (compare(entry.wanted, entry.latest) < 0) {
  561. writeLine(name);
  562. /* eslint-disable no-await-in-loop */
  563. await exec(`npm install --quiet --no-progress --silent --save-exact ${name}@latest`);
  564. /* eslint-enable no-await-in-loop */
  565. }
  566. }
  567. }
  568. let targets = [
  569. 'armnn',
  570. 'bigdl',
  571. 'caffe', 'circle', 'cntk', 'coreml',
  572. 'dlc', 'dnn',
  573. 'executorch',
  574. 'gguf',
  575. 'kann', 'keras',
  576. 'mlir', 'mnn', 'mslite', 'megengine',
  577. 'nnabla',
  578. 'onnx', 'om',
  579. 'paddle', 'pytorch',
  580. 'rknn',
  581. 'sentencepiece', 'sklearn',
  582. 'tf',
  583. 'uff',
  584. 'xmodel'
  585. ];
  586. let commands = [
  587. 'sync',
  588. 'install',
  589. 'schema',
  590. 'metadata'
  591. ];
  592. if (filter.size > 0 && targets.some((target) => filter.has(target))) {
  593. targets = targets.filter((target) => filter.has(target));
  594. }
  595. if (filter.size > 0 && commands.some((target) => filter.has(target))) {
  596. commands = commands.filter((command) => filter.has(command));
  597. }
  598. commands = commands.join(' ');
  599. for (const target of targets) {
  600. /* eslint-disable no-await-in-loop */
  601. await exec(`tools/${target} ${commands}`);
  602. /* eslint-enable no-await-in-loop */
  603. }
  604. };
  605. const pull = async () => {
  606. await exec('git fetch --prune origin "refs/tags/*:refs/tags/*"');
  607. let before = await exec('git rev-parse HEAD', 'utf-8');
  608. if (before.status !== 0) {
  609. throw new Error(before.stderr.trim());
  610. }
  611. try {
  612. await exec('git pull --prune --rebase --autostash');
  613. } catch (error) {
  614. writeLine(error.message);
  615. }
  616. let after = await exec('git rev-parse HEAD', 'utf-8');
  617. if (after.status !== 0) {
  618. throw new Error(after.stderr.trim());
  619. }
  620. before = before.stdout.trim();
  621. after = after.stdout.trim();
  622. if (before !== after) {
  623. const output = await exec(`git diff --name-only ${before} ${after}`, 'utf-8');
  624. const files = new Set(output.stdout.split('\n'));
  625. if (files.has('package.json')) {
  626. await clean();
  627. await install();
  628. }
  629. }
  630. };
  631. const coverage = async () => {
  632. switch (read()) {
  633. case 'desktop': {
  634. await rm('dist', 'nyc');
  635. await mkdir('dist', 'nyc');
  636. await exec('cp package.json dist/nyc');
  637. await exec('cp -R source dist/nyc');
  638. await exec('nyc instrument --compact false source dist/nyc/source');
  639. const file = dirname('dist', 'nyc', 'source', 'index.html');
  640. let content = await fs.readFile(file, 'utf-8');
  641. content = content.replace(`"script-src 'self'"`, `"script-src 'self' 'unsafe-eval'"`);
  642. await fs.writeFile(file, content, 'utf-8');
  643. await exec('nyc --instrument npx electron ./dist/nyc');
  644. await exec('nyc report');
  645. break;
  646. }
  647. case 'test': {
  648. const target = args.join(' ');
  649. await rm('dist', 'c8');
  650. await exec(`npx c8 --reporter=html --report-dir=dist/c8/report node test/models.js ${target}`);
  651. break;
  652. }
  653. default: {
  654. throw new Error('Unsupported coverage target.');
  655. }
  656. }
  657. };
  658. const analyze = async () => {
  659. const exists = await access('third_party/tools/codeql');
  660. if (!exists) {
  661. await exec('git clone --depth=1 https://github.com/github/codeql.git third_party/tools/codeql');
  662. }
  663. await rm('dist', 'codeql');
  664. await mkdir('dist', 'codeql', 'netron');
  665. await exec('cp -r publish source test tools dist/codeql/netron/');
  666. await exec('codeql database create dist/codeql/database --source-root dist/codeql/netron --language=javascript --threads=3');
  667. await exec('codeql database analyze dist/codeql/database ./third_party/tools/codeql/javascript/ql/src/codeql-suites/javascript-security-and-quality.qls --format=csv --output=dist/codeql/results.csv --threads=3');
  668. await exec('cat dist/codeql/results.csv');
  669. };
  670. const version = async () => {
  671. await pull();
  672. const file = dirname('package.json');
  673. let content = await fs.readFile(file, 'utf-8');
  674. content = content.replace(/(\s*"version":\s")(\d\.\d\.\d)(",)/m, (match, p1, p2, p3) => {
  675. const version = Array.from((parseInt(p2.split('.').join(''), 10) + 1).toString()).join('.');
  676. return p1 + version + p3;
  677. });
  678. content = content.replace(/(\s*"date":\s")(.*)(",)/m, (match, p1, p2, p3) => {
  679. const date = new Date().toISOString().split('.').shift().split('T').join(' ');
  680. return p1 + date + p3;
  681. });
  682. await fs.writeFile(file, content, 'utf-8');
  683. await exec('npm install --package-lock-only');
  684. await load();
  685. await exec('git add package.json');
  686. await exec('git add package-lock.json');
  687. await exec(`git commit -m "Update to ${configuration.version}"`);
  688. await exec(`git tag v${configuration.version}`);
  689. await exec('git push');
  690. await exec('git push --tags');
  691. };
  692. const main = async () => {
  693. await load();
  694. try {
  695. const task = read();
  696. switch (task) {
  697. case 'start': await start(); break;
  698. case 'clean': await clean(); break;
  699. case 'install': await install(); break;
  700. case 'build': await build(); break;
  701. case 'publish': await publish(); break;
  702. case 'version': await version(); break;
  703. case 'lint': await lint(); break;
  704. case 'test': await test(); break;
  705. case 'validate': await validate(); break;
  706. case 'update': await update(); break;
  707. case 'pull': await pull(); break;
  708. case 'analyze': await analyze(); break;
  709. case 'coverage': await coverage(); break;
  710. default: throw new Error(`Unsupported task '${task}'.`);
  711. }
  712. } catch (error) {
  713. if (process.stdout.write) {
  714. process.stdout.write(error.message + os.EOL);
  715. }
  716. process.exit(1);
  717. }
  718. };
  719. await main();