From 32ed8a9b761e693cc5b6f36c0bd702860c850f74 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 11 Jun 2023 19:38:17 -0500 Subject: [PATCH] feat: improve typescript build process; automatically (re)build replicant schemas (#93) --- .github/workflows/ci.yml | 8 +- src/generators/app/index.ts | 11 +- src/generators/app/templates/.eslintignore | 2 + .../app/templates/scripts/build.mjs | 192 ++++++++++-------- .../app/templates/scripts/debounce.mjs | 19 ++ .../src/types/schemas/exampleReplicant.d.ts | 2 +- test/app.spec.ts | 21 -- 7 files changed, 147 insertions(+), 108 deletions(-) create mode 100644 src/generators/app/templates/.eslintignore create mode 100644 src/generators/app/templates/scripts/debounce.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 449bd63..531059a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: key: npm-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Install dependencies - run: yarn --ignore-engines --frozen-lockfile --network-timeout 1000000 + run: npm ci - name: Lint run: | @@ -36,6 +36,7 @@ jobs: build-test: strategy: + fail-fast: false matrix: node-version: [16.x, 18.x] os: [ubuntu-latest, windows-latest, macos-latest] @@ -58,12 +59,15 @@ jobs: key: npm-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Install dependencies - run: yarn --ignore-engines --frozen-lockfile --network-timeout 1000000 + run: npm ci - name: Build run: | npm run build + - name: Install nodecg-cli to use in tests + run: npm i -g nodecg-cli@latest + - name: Test run: | npm run test diff --git a/src/generators/app/index.ts b/src/generators/app/index.ts index e1dd14a..8590ae4 100644 --- a/src/generators/app/index.ts +++ b/src/generators/app/index.ts @@ -209,7 +209,8 @@ export default class AppGenerator extends Generator { 'build:extension': 'node scripts/build.mjs --extension', watch: 'node scripts/build.mjs --all --watch', 'watch:browser': 'node scripts/build.mjs --dashboard --graphics --watch', - dev: 'concurrently --kill-others "npm run watch:browser" "nodemon"', + 'watch:schemas': 'node scripts/build.mjs --schemas --watch', + dev: 'concurrently --kill-others "npm run watch:schemas" "npm run watch:browser" "nodemon"', 'generate-schema-types': 'trash src/types/schemas && nodecg schema-types', }; /* eslint-enable @typescript-eslint/naming-convention */ @@ -253,6 +254,10 @@ export default class AppGenerator extends Generator { this.fs.copy(this.templatePath('scripts/build.mjs'), this.destinationPath('scripts/build.mjs')); } + if (!this.fs.exists(this.destinationPath('scripts/debounce.mjs'))) { + this.fs.copy(this.templatePath('scripts/debounce.mjs'), this.destinationPath('scripts/debounce.mjs')); + } + if (!this.fs.exists(this.destinationPath('.parcelrc'))) { this.fs.copy(this.templatePath('.parcelrc'), this.destinationPath('.parcelrc')); } @@ -282,6 +287,10 @@ export default class AppGenerator extends Generator { this.fs.copy(this.templatePath('nodemon.json'), this.destinationPath('nodemon.json')); } + if (!this.fs.exists(this.destinationPath('.eslintignore'))) { + this.fs.copy(this.templatePath('.eslintignore'), this.destinationPath('.eslintignore')); + } + await this.addDependencies(['ts-node']); await this.addDevDependencies([ 'typescript', diff --git a/src/generators/app/templates/.eslintignore b/src/generators/app/templates/.eslintignore new file mode 100644 index 0000000..a8753a6 --- /dev/null +++ b/src/generators/app/templates/.eslintignore @@ -0,0 +1,2 @@ +node_modules +/scripts diff --git a/src/generators/app/templates/scripts/build.mjs b/src/generators/app/templates/scripts/build.mjs index 29924cb..b8696cc 100644 --- a/src/generators/app/templates/scripts/build.mjs +++ b/src/generators/app/templates/scripts/build.mjs @@ -6,115 +6,141 @@ // Native import { fileURLToPath } from 'url'; import { argv } from 'process'; +import { execSync } from 'child_process'; // Packages import { glob } from 'glob'; import { Parcel } from '@parcel/core'; +import chokidar from 'chokidar'; // Ours import pjson from '../package.json' assert { type: 'json' }; +import debounce from './debounce.mjs'; const buildAll = argv.includes('--all'); const buildExtension = argv.includes('--extension') || buildAll; const buildDashboard = argv.includes('--dashboard') || buildAll; const buildGraphics = argv.includes('--graphics') || buildAll; +const buildSchemas = argv.includes('--schemas') || buildAll; const bundlers = new Set(); const commonBrowserTargetProps = { - engines: { - browsers: ['last 5 Chrome versions'], - }, - context: 'browser', + engines: { + browsers: ['last 5 Chrome versions'], + }, + context: 'browser', }; if (buildDashboard) { - bundlers.add( - new Parcel({ - entries: glob.sync('src/dashboard/**/*.html'), - targets: { - default: { - ...commonBrowserTargetProps, - distDir: 'dashboard', - publicUrl: `/bundles/${pjson.name}/dashboard`, - }, - }, - defaultConfig: '@parcel/config-default', - additionalReporters: [ - { - packageName: '@parcel/reporter-cli', - resolveFrom: fileURLToPath(import.meta.url), - }, - ], - }), - ); + bundlers.add( + new Parcel({ + entries: glob.sync('src/dashboard/**/*.html'), + targets: { + default: { + ...commonBrowserTargetProps, + distDir: 'dashboard', + publicUrl: `/bundles/${pjson.name}/dashboard`, + }, + }, + defaultConfig: '@parcel/config-default', + additionalReporters: [ + { + packageName: '@parcel/reporter-cli', + resolveFrom: fileURLToPath(import.meta.url), + }, + ], + }), + ); } if (buildGraphics) { - bundlers.add( - new Parcel({ - entries: glob.sync('src/graphics/**/*.html'), - targets: { - default: { - ...commonBrowserTargetProps, - distDir: 'graphics', - publicUrl: `/bundles/${pjson.name}/graphics`, - }, - }, - defaultConfig: '@parcel/config-default', - additionalReporters: [ - { - packageName: '@parcel/reporter-cli', - resolveFrom: fileURLToPath(import.meta.url), - }, - ], - }), - ); + bundlers.add( + new Parcel({ + entries: glob.sync('src/graphics/**/*.html'), + targets: { + default: { + ...commonBrowserTargetProps, + distDir: 'graphics', + publicUrl: `/bundles/${pjson.name}/graphics`, + }, + }, + defaultConfig: '@parcel/config-default', + additionalReporters: [ + { + packageName: '@parcel/reporter-cli', + resolveFrom: fileURLToPath(import.meta.url), + }, + ], + }), + ); } if (buildExtension) { - bundlers.add( - new Parcel({ - entries: 'src/extension/index.ts', - targets: { - default: { - context: 'node', - distDir: 'extension', - }, - }, - defaultConfig: '@parcel/config-default', - additionalReporters: [ - { - packageName: '@parcel/reporter-cli', - resolveFrom: fileURLToPath(import.meta.url), - }, - ], - }), - ); + bundlers.add( + new Parcel({ + entries: 'src/extension/index.ts', + targets: { + default: { + context: 'node', + distDir: 'extension', + }, + }, + defaultConfig: '@parcel/config-default', + additionalReporters: [ + { + packageName: '@parcel/reporter-cli', + resolveFrom: fileURLToPath(import.meta.url), + }, + ], + }), + ); } try { - if (argv.includes('--watch')) { - const watchPromises = []; - for (const bundler of bundlers.values()) { - watchPromises.push( - bundler.watch((err) => { - if (err) { - // fatal error - throw err; - } - }), - ); - } - await Promise.all(watchPromises); - } else { - const buildPromises = []; - for (const bundler of bundlers.values()) { - buildPromises.push(bundler.run()); - } - await Promise.all(buildPromises); - } - console.log('Bundle build completed successfully'); + if (argv.includes('--watch')) { + if (buildSchemas) { + watchSchemas(); + } + + const watchPromises = []; + for (const bundler of bundlers.values()) { + watchPromises.push( + bundler.watch((err) => { + if (err) { + // fatal error + throw err; + } + }), + ); + } + + await Promise.all(watchPromises); + } else { + if (buildSchemas) { + doBuildSchemas(); + } + + const buildPromises = []; + for (const bundler of bundlers.values()) { + buildPromises.push(bundler.run()); + } + + await Promise.all(buildPromises); + } + + console.log('Bundle build completed successfully'); } catch (_) { - // the reporter-cli package will handle printing errors to the user - process.exit(1); + // the reporter-cli package will handle printing errors to the user + process.exit(1); +} + +function doBuildSchemas() { + execSync('npm run generate-schema-types'); + process.stdout.write(`🔧 Built Replicant schema types!\n`); +} + +function watchSchemas() { + chokidar.watch('schemas/**/*.json').on('all', () => { + debounce('compileSchemas', doBuildSchemas); + }); } diff --git a/src/generators/app/templates/scripts/debounce.mjs b/src/generators/app/templates/scripts/debounce.mjs new file mode 100644 index 0000000..b14eeba --- /dev/null +++ b/src/generators/app/templates/scripts/debounce.mjs @@ -0,0 +1,19 @@ +const timers = new Map(); + +/** + * A standard debounce, but uses a string `name` as the key instead of the callback. + */ +export default function (name, callback, duration = 500) { + const existing = timers.get(name); + if (existing) { + clearTimeout(existing); + } + + timers.set( + name, + setTimeout(() => { + timers.delete(name); + callback(); + }, duration), + ); +} diff --git a/src/generators/app/templates/src/types/schemas/exampleReplicant.d.ts b/src/generators/app/templates/src/types/schemas/exampleReplicant.d.ts index 6c52ade..64271c5 100644 --- a/src/generators/app/templates/src/types/schemas/exampleReplicant.d.ts +++ b/src/generators/app/templates/src/types/schemas/exampleReplicant.d.ts @@ -1,4 +1,4 @@ -/* tslint:disable */ +/* eslint:disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, diff --git a/test/app.spec.ts b/test/app.spec.ts index b79ae26..d2ddb3e 100644 --- a/test/app.spec.ts +++ b/test/app.spec.ts @@ -183,27 +183,6 @@ describe('nodecg:app', () => { assert.fileContent('typescript-bundle/package.json', '"concurrently"'); }); - it('adds build scripts to package.json', () => { - assert.fileContent('typescript-bundle/package.json', '"build": "node scripts/build.mjs"'); - assert.fileContent( - 'typescript-bundle/package.json', - '"build:extension": "node scripts/build.mjs --skipBrowser"', - ); - assert.fileContent('typescript-bundle/package.json', '"watch": "node scripts/build.mjs --watch"'); - assert.fileContent( - 'typescript-bundle/package.json', - '"watch:browser": "node scripts/build.mjs --skipExtension --watch"', - ); - assert.fileContent( - 'typescript-bundle/package.json', - `"dev": "concurrently --kill-others \\"npm run watch:browser\\" \\"nodemon\\""`, - ); - assert.fileContent( - 'typescript-bundle/package.json', - '"generate-schema-types": "trash src/types/schemas && nodecg schema-types"', - ); - }); - it('generates an actually buildable bundle', async function () { // Increase timeout because npm install (and build) can take some time... this.timeout(300000); // 5 minutes