@lightningjs/blits 1.26.1 → 1.27.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/.eslintrc.cjs CHANGED
@@ -52,7 +52,6 @@ module.exports = {
52
52
  },
53
53
  parser: '@babel/eslint-parser',
54
54
  parserOptions: {
55
- parser: 'babel-eslint',
56
55
  ecmaVersion: 2018,
57
56
  sourceType: 'module',
58
57
  requireConfigFile: false,
@@ -0,0 +1,151 @@
1
+ name: Comment ESLint Results
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["Run ESLint"]
6
+ types:
7
+ - completed
8
+
9
+ jobs:
10
+ comment-lint-results:
11
+ # Only run this job if the previous workflow failed (indicating lint errors)
12
+ if: github.event.workflow_run.conclusion == 'failure'
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ pull-requests: write
16
+
17
+ steps:
18
+ - name: Download Lint Results
19
+ id: download
20
+ uses: actions/github-script@v6
21
+ with:
22
+ script: |
23
+ try {
24
+ core.info('Downloading lint results artifact');
25
+
26
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
27
+ owner: context.repo.owner,
28
+ repo: context.repo.repo,
29
+ run_id: ${{ github.event.workflow_run.id }}
30
+ });
31
+
32
+ const matchArtifact = artifacts.data.artifacts.find(artifact => artifact.name === "lint-results");
33
+ if (!matchArtifact) {
34
+ core.setFailed('No lint results artifact found');
35
+ return false;
36
+ }
37
+
38
+ core.info(`Found artifact id: ${matchArtifact.id}`);
39
+
40
+ const download = await github.rest.actions.downloadArtifact({
41
+ owner: context.repo.owner,
42
+ repo: context.repo.repo,
43
+ artifact_id: matchArtifact.id,
44
+ archive_format: 'zip'
45
+ });
46
+
47
+ const fs = require('fs');
48
+ try {
49
+ fs.writeFileSync('lint-results.zip', Buffer.from(download.data));
50
+ core.info('Successfully downloaded and saved artifact');
51
+ return true;
52
+ } catch (error) {
53
+ core.setFailed(`Failed to write artifact: ${error.message}`);
54
+ return false;
55
+ }
56
+ } catch (error) {
57
+ core.setFailed(`Failed to download artifact: ${error.message}`);
58
+ return false;
59
+ }
60
+ result-encoding: string
61
+
62
+ - name: Extract Lint Results
63
+ if: steps.download.outputs.result == 'true'
64
+ id: extract
65
+ run: |
66
+ mkdir -p lint-results
67
+ unzip -o lint-results.zip -d lint-results
68
+
69
+ if [ -f lint-results/errors.md ]; then
70
+ # Use GitHub's EOF syntax for multiline content
71
+ echo "LINT_ERRORS<<EOF" >> $GITHUB_ENV
72
+ cat lint-results/errors.md >> $GITHUB_ENV
73
+ echo "EOF" >> $GITHUB_ENV
74
+ echo "HAS_ERRORS=true" >> $GITHUB_OUTPUT
75
+ else
76
+ echo "HAS_ERRORS=false" >> $GITHUB_OUTPUT
77
+ fi
78
+
79
+ - name: Find PR Number
80
+ if: steps.extract.outputs.HAS_ERRORS == 'true'
81
+ id: find-pr
82
+ uses: actions/github-script@v6
83
+ with:
84
+ script: |
85
+ try {
86
+ core.info('Finding associated PR');
87
+
88
+ const { owner, repo } = context.repo;
89
+ const run_id = ${{ github.event.workflow_run.id }};
90
+
91
+ // Get the triggering workflow run
92
+ const run = await github.rest.actions.getWorkflowRun({
93
+ owner,
94
+ repo,
95
+ run_id
96
+ });
97
+
98
+ // Find associated PR - first try the event payload
99
+ if (run.data.pull_requests && run.data.pull_requests.length > 0) {
100
+ core.info(`Found PR directly from run data: #${run.data.pull_requests[0].number}`);
101
+ return run.data.pull_requests[0].number;
102
+ }
103
+
104
+ // Fallback to searching by head SHA
105
+ core.info(`Searching for PR using head SHA: ${run.data.head_sha}`);
106
+ const pulls = await github.rest.pulls.list({
107
+ owner,
108
+ repo,
109
+ state: 'open',
110
+ head: `${owner}:${run.data.head_branch}`
111
+ });
112
+
113
+ if (pulls.data.length > 0) {
114
+ core.info(`Found PR by branch: #${pulls.data[0].number}`);
115
+ return pulls.data[0].number;
116
+ }
117
+
118
+ core.info('No PR found for this workflow run');
119
+ return null;
120
+ } catch (error) {
121
+ core.setFailed(`Failed to find PR: ${error.message}`);
122
+ return null;
123
+ }
124
+ result-encoding: string
125
+
126
+ - name: Post Lint Results as PR Comment
127
+ if: steps.find-pr.outputs.result != 'null' && steps.find-pr.outputs.result != ''
128
+ uses: actions/github-script@v6
129
+ with:
130
+ script: |
131
+ try {
132
+ const pr_number = parseInt(${{ steps.find-pr.outputs.result }});
133
+
134
+ if (isNaN(pr_number)) {
135
+ core.warning('Invalid PR number');
136
+ return;
137
+ }
138
+
139
+ core.info(`Posting comment to PR #${pr_number}`);
140
+
141
+ await github.rest.issues.createComment({
142
+ owner: context.repo.owner,
143
+ repo: context.repo.repo,
144
+ issue_number: pr_number,
145
+ body: process.env.LINT_ERRORS
146
+ });
147
+
148
+ core.info(`Successfully posted lint errors comment to PR #${pr_number}`);
149
+ } catch (error) {
150
+ core.setFailed(`Failed to post comment: ${error.message}`);
151
+ }
@@ -0,0 +1,114 @@
1
+ name: Comment Test Results
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["Run Blits Tests"]
6
+ types:
7
+ - completed
8
+
9
+ jobs:
10
+ comment-test-results:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ pull-requests: write
14
+
15
+ steps:
16
+ - name: Download Test Results
17
+ uses: actions/github-script@v6
18
+ with:
19
+ script: |
20
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
21
+ owner: context.repo.owner,
22
+ repo: context.repo.repo,
23
+ run_id: ${{ github.event.workflow_run.id }}
24
+ });
25
+
26
+ const matchArtifact = artifacts.data.artifacts.find(artifact => artifact.name === "test-results");
27
+ if (!matchArtifact) {
28
+ core.setFailed('No test results artifact found');
29
+ return;
30
+ }
31
+
32
+ const download = await github.rest.actions.downloadArtifact({
33
+ owner: context.repo.owner,
34
+ repo: context.repo.repo,
35
+ artifact_id: matchArtifact.id,
36
+ archive_format: 'zip'
37
+ });
38
+
39
+ const fs = require('fs');
40
+ fs.writeFileSync('test-results.zip', Buffer.from(download.data));
41
+
42
+ - name: Extract Test Results
43
+ run: |
44
+ mkdir -p test-results
45
+ unzip -o test-results.zip -d test-results
46
+
47
+ TIMESTAMP=$(cat test-results/timestamp.txt)
48
+ SUMMARY=$(cat test-results/summary.txt)
49
+ FAILED=$(cat test-results/failed.txt)
50
+
51
+ echo "TIMESTAMP=$TIMESTAMP" >> $GITHUB_ENV
52
+ echo "SUMMARY=$SUMMARY" >> $GITHUB_ENV
53
+ echo "FAILED=$FAILED" >> $GITHUB_ENV
54
+
55
+ # Check if error.txt exists and read it if it does
56
+ if [ -f test-results/error.txt ]; then
57
+ # Store raw error output in a file for the next step
58
+ cat test-results/error.txt > raw_error.txt
59
+ fi
60
+
61
+ - name: Find PR Number
62
+ id: find-pr
63
+ uses: actions/github-script@v6
64
+ with:
65
+ script: |
66
+ const { owner, repo } = context.repo;
67
+ const run_id = ${{ github.event.workflow_run.id }};
68
+
69
+ // Get the triggering workflow run
70
+ const run = await github.rest.actions.getWorkflowRun({
71
+ owner,
72
+ repo,
73
+ run_id
74
+ });
75
+
76
+ // Find associated PR
77
+ const pulls = await github.rest.pulls.list({
78
+ owner,
79
+ repo,
80
+ state: 'open',
81
+ head: run.data.head_sha
82
+ });
83
+
84
+ if (pulls.data.length > 0) {
85
+ return pulls.data[0].number;
86
+ }
87
+
88
+ console.log('No PR found');
89
+ return null;
90
+ result-encoding: string
91
+
92
+ - name: Post Test Results as PR Comment
93
+ if: steps.find-pr.outputs.result != 'null'
94
+ uses: actions/github-script@v6
95
+ with:
96
+ script: |
97
+ const fs = require('fs');
98
+ const pr_number = parseInt(${{ steps.find-pr.outputs.result }});
99
+ const status = process.env.FAILED === 'true' ? '❌ FAILED' : '✅ PASSED';
100
+
101
+ let commentBody = `#### Test Results: ${status}\n**Run at:** ${process.env.TIMESTAMP}\n\n**Summary:**\n${process.env.SUMMARY}`;
102
+
103
+ // Add error output if it exists
104
+ if (fs.existsSync('raw_error.txt')) {
105
+ const errorOutput = fs.readFileSync('raw_error.txt', 'utf8');
106
+ commentBody += '\n\n**Error Output:**\n```\n' + errorOutput + '\n```';
107
+ }
108
+
109
+ github.rest.issues.createComment({
110
+ owner: context.repo.owner,
111
+ repo: context.repo.repo,
112
+ issue_number: pr_number,
113
+ body: commentBody
114
+ });
@@ -1,7 +1,7 @@
1
1
  name: Run Blits Tests
2
2
 
3
3
  on:
4
- pull_request_target:
4
+ pull_request:
5
5
  branches:
6
6
  - dev
7
7
  - master
@@ -9,13 +9,12 @@ on:
9
9
  jobs:
10
10
  run-blits-tests:
11
11
  runs-on: ubuntu-latest
12
+ outputs:
13
+ tests_failed: ${{ steps.analyze-results.outputs.result && fromJSON(steps.analyze-results.outputs.result).testsFailed }}
12
14
 
13
15
  steps:
14
16
  - name: Checkout Code
15
17
  uses: actions/checkout@v3
16
- with:
17
- ref: ${{ github.event.pull_request.head.ref }}
18
- repository: ${{ github.event.pull_request.head.repo.full_name }}
19
18
 
20
19
  - name: Set up Node.js
21
20
  uses: actions/setup-node@v3
@@ -26,44 +25,92 @@ jobs:
26
25
  run: npm install
27
26
 
28
27
  - name: Run Tests
29
- run: npm run test > test-report.txt
30
- #To ensure subsequent steps run even if tests fail
31
- continue-on-error: true
28
+ id: run-tests
29
+ run: npm run test > test-report.txt 2>&1 || echo "Command failed with exit code $?" >> test-report.txt
32
30
 
33
- - name: Get Current Timestamp
34
- id: timestamp
35
- run: echo "timestamp=$(date)" >> $GITHUB_OUTPUT
31
+ - name: Analyze Test Results
32
+ id: analyze-results
33
+ uses: actions/github-script@v6
34
+ with:
35
+ result-encoding: json
36
+ script: |
37
+ const fs = require('fs');
38
+ const timestamp = new Date().toISOString();
36
39
 
37
- - name: Extract Test Summary
38
- id: extracted-test-summary
39
- run: |
40
- # Search for a line containing both "passed:" and "failed:"
41
- summary=$(grep -E "passed:.*failed:" test-report.txt | head -n1)
40
+ const results = {
41
+ timestamp,
42
+ summary: '',
43
+ testsFailed: false,
44
+ error: null
45
+ };
42
46
 
43
- # Extract the number of failed tests
44
- failed=$(echo "$summary" | sed -E 's/.*failed:\s*([0-9]+).*/\1/')
47
+ // Check if test report exists and has content
48
+ if (!fs.existsSync('test-report.txt') || !fs.statSync('test-report.txt').size) {
49
+ results.summary = 'Test execution error: No test output generated.';
50
+ results.testsFailed = true;
51
+ return outputResults(results);
52
+ }
45
53
 
46
- echo "summary=$summary" >> $GITHUB_OUTPUT
54
+ // Read test report
55
+ const testOutput = fs.readFileSync('test-report.txt', 'utf8');
47
56
 
48
- # If the number of failed tests is greater than 1, exit with error.
49
- if [ "$failed" -gt 0 ]; then
50
- exit 1
51
- fi
57
+ // Check for errors and test results
58
+ const hasError = testOutput.includes('Error');
59
+ const testSummaryMatch = testOutput.match(/passed:\s*(\d+)\s*failed:\s*(\d+)\s*of\s*(\d+)\s*tests/);
52
60
 
53
- - name: Post Test Results as PR Comment
54
- if: always()
55
- uses: actions/github-script@v6
56
- env:
57
- SUMMARY: ${{ steps.extracted-test-summary.outputs.summary }}
58
- TIMESTAMP: ${{ steps.timestamp.outputs.timestamp }}
59
- with:
60
- script: |
61
- github.rest.issues.createComment({
62
- owner: context.repo.owner,
63
- repo: context.repo.repo,
64
- issue_number: context.issue.number,
65
- body: `**Test Results Summary:** (Ran at: ${process.env.TIMESTAMP})\n${process.env.SUMMARY}`
66
- });
61
+ if (hasError && (!testSummaryMatch || testSummaryMatch[3] === '0')) {
62
+ // Error with no tests or zero tests
63
+ results.summary = 'Test execution encountered errors. No tests were run.';
64
+ results.testsFailed = true;
65
+ results.error = testOutput;
66
+ } else if (testSummaryMatch) {
67
+ // We have a test summary
68
+ const passed = parseInt(testSummaryMatch[1]);
69
+ const failed = parseInt(testSummaryMatch[2]);
70
+ const total = parseInt(testSummaryMatch[3]);
71
+
72
+ results.summary = testSummaryMatch[0];
73
+
74
+ if (failed > 0 || total === 0) {
75
+ results.testsFailed = true;
76
+
77
+ if (total === 0) {
78
+ results.summary = 'No tests were found to run. This is considered a failure.';
79
+ }
80
+ }
81
+ } else {
82
+ // No test summary found
83
+ results.summary = 'Test execution produced no valid test summary';
84
+ results.testsFailed = true;
85
+ results.error = testOutput;
86
+ }
87
+
88
+ return outputResults(results);
89
+
90
+ /**
91
+ * Outputs results and saves result files
92
+ */
93
+ function outputResults(results) {
94
+ // Create test results directory and files
95
+ try {
96
+ fs.mkdirSync('./test-results', { recursive: true });
97
+
98
+ // Write results files
99
+ fs.writeFileSync('./test-results/timestamp.txt', results.timestamp);
100
+ fs.writeFileSync('./test-results/summary.txt', results.summary);
101
+ fs.writeFileSync('./test-results/failed.txt', results.testsFailed.toString());
102
+
103
+ // Save error output if it exists
104
+ if (results.error) {
105
+ fs.writeFileSync('./test-results/error.txt', results.error);
106
+ }
107
+ } catch (err) {
108
+ console.error('Error saving test results:', err);
109
+ core.warning(`Error saving test results: ${err.message}`);
110
+ }
111
+
112
+ return results;
113
+ }
67
114
 
68
115
  - name: Upload Test Report Artifact
69
116
  if: always()
@@ -71,3 +118,15 @@ jobs:
71
118
  with:
72
119
  name: test-report
73
120
  path: test-report.txt
121
+
122
+ - name: Upload Test Results
123
+ if: always()
124
+ uses: actions/upload-artifact@v4
125
+ with:
126
+ name: test-results
127
+ path: ./test-results/
128
+
129
+ # Fail the workflow if tests failed
130
+ - name: Check Test Status
131
+ if: ${{ steps.analyze-results.outputs.result && fromJSON(steps.analyze-results.outputs.result).testsFailed }}
132
+ run: exit 1
@@ -0,0 +1,118 @@
1
+ name: Run ESLint
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - dev
7
+ - master
8
+
9
+ jobs:
10
+ run-lint:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+
15
+ steps:
16
+ - name: Checkout Code
17
+ uses: actions/checkout@v3
18
+ with:
19
+ # Needed to compare changes against base branch
20
+ fetch-depth: 0
21
+
22
+ - name: Set up Node.js
23
+ uses: actions/setup-node@v3
24
+ with:
25
+ node-version: 20.16.0
26
+
27
+ - name: Install Dependencies
28
+ run: npm install
29
+
30
+ - name: Get list of changed JS files
31
+ id: changed-files
32
+ run: |
33
+ # Get the list of changed .js files compared to the base branch
34
+ git diff --name-only --diff-filter=ACMRT origin/${{ github.base_ref }} HEAD | grep '\.js$' > js-changed-files.txt || true
35
+
36
+ if [ -s js-changed-files.txt ]; then
37
+ echo "has_js_changes=true" >> $GITHUB_OUTPUT
38
+ cat js-changed-files.txt | tr '\n' ' ' > js-changed-files-spaces.txt
39
+ echo "files=$(cat js-changed-files-spaces.txt)" >> $GITHUB_OUTPUT
40
+ else
41
+ echo "has_js_changes=false" >> $GITHUB_OUTPUT
42
+ fi
43
+
44
+ - name: Run ESLint on changed files
45
+ id: eslint
46
+ if: steps.changed-files.outputs.has_js_changes == 'true'
47
+ run: |
48
+ # Create directory for output
49
+ mkdir -p ./lint-results
50
+
51
+ # Run ESLint only on changed files with the project's config
52
+ # Filtering out warnings - only keep errors
53
+ npx eslint --config .eslintrc.cjs ${{ steps.changed-files.outputs.files }} -f json > ./lint-results/eslint-output.json || true
54
+
55
+ # Check if there are any errors in the ESLint output
56
+ if [ -s ./lint-results/eslint-output.json ]; then
57
+ error_count=$(node -e "
58
+ const results = require('./lint-results/eslint-output.json');
59
+ let errorCount = 0;
60
+ for (const file of results) {
61
+ errorCount += file.errorCount || 0;
62
+ }
63
+ console.log(errorCount);
64
+ ")
65
+
66
+ echo "errors_found=$error_count" >> $GITHUB_OUTPUT
67
+
68
+ if [ "$error_count" -gt 0 ]; then
69
+ # Create the formatted error report
70
+ echo '❌ **Linting errors found**' > ./lint-results/errors.md
71
+ echo '' >> ./lint-results/errors.md
72
+ echo 'These linting errors must be fixed before this PR can be merged:' >> ./lint-results/errors.md
73
+ echo '' >> ./lint-results/errors.md
74
+ echo '```' >> ./lint-results/errors.md
75
+
76
+ # Extract and format errors from the JSON
77
+ error_count=$(node -e "
78
+ const results = require('./lint-results/eslint-output.json');
79
+ let errorSummary = '';
80
+
81
+ for (const file of results) {
82
+ if (file.errorCount > 0) {
83
+ const filePath = file.filePath.replace(process.env.GITHUB_WORKSPACE + '/', '');
84
+ errorSummary += filePath + '\\n';
85
+
86
+ for (const msg of file.messages) {
87
+ if (msg.severity === 2) {
88
+ errorSummary += ' Line ' + msg.line + ':' + msg.column + ': ' +
89
+ msg.message + ' (' + msg.ruleId + ')\\n';
90
+ }
91
+ }
92
+ errorSummary += '\\n';
93
+ }
94
+ }
95
+
96
+ console.log(errorSummary.trim());
97
+ ")
98
+
99
+ echo "$error_count" >> ./lint-results/errors.md
100
+ echo '```' >> ./lint-results/errors.md
101
+ echo '' >> ./lint-results/errors.md
102
+ echo '_Run `npm run lint:fix` to automatically fix some of these issues._' >> ./lint-results/errors.md
103
+ fi
104
+ else
105
+ echo "errors_found=0" >> $GITHUB_OUTPUT
106
+ fi
107
+
108
+ - name: Upload Lint Results
109
+ if: steps.changed-files.outputs.has_js_changes == 'true'
110
+ uses: actions/upload-artifact@v4
111
+ with:
112
+ name: lint-results
113
+ path: ./lint-results/
114
+
115
+ # Fail the workflow if linting errors found
116
+ - name: Check Lint Status
117
+ if: steps.changed-files.outputs.has_js_changes == 'true' && steps.eslint.outputs.errors_found != '0'
118
+ run: exit 1
@@ -0,0 +1 @@
1
+ npx lint-staged
@@ -0,0 +1 @@
1
+ node ./scripts/prepush.js
package/CHANGELOG.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ _11 apr 2025_
4
+
5
+ ## v1.27.0
6
+
7
+ - Added pre push linting check
8
+ - Updated Github workflow for tests
9
+ - Added noop announcement for when announcer is disabled
10
+ - Add ability to interrupt specific announcement messages
11
+
3
12
  _09 apr 2025_
4
13
 
5
- ## v1.26.1
14
+ ## v1.26.1 / v1.26.2
6
15
 
7
16
  - Fixed issue with announcer queue
8
17
 
@@ -81,6 +81,8 @@ Imagine an App with a row of tiles, it's possible that before the title of the r
81
81
 
82
82
  The `speak()`-method return a Promise that also contains a `cancel()` function. When called, it will cancel that specific message and remove it from the queue before it can be spoken out.
83
83
 
84
+ Additionally if you want to _interrupt_ a specific messages as it's being spoken out as well and go straight to the next message in the queue (i.e. the newly focused item, for example). You can use the `stop()` message that is returned on the Promise returned by the `speak()`-method.
85
+
84
86
  ```js
85
87
  Blits.Component('MyTile', {
86
88
  //
@@ -96,7 +98,9 @@ Blits.Component('MyTile', {
96
98
  this.message = this.$announcer.speak(`This is tile ${this.title}`)
97
99
  },
98
100
  unfocus() {
99
- // when unfocused cancel the message and remove it from the queue
101
+ // when unfocused interrupt the message if it's already being spoken out
102
+ this.message.stop()
103
+ // and cancel the message to remove it from the queue
100
104
  this.message.cancel()
101
105
  }
102
106
  }
package/index.d.ts CHANGED
@@ -31,6 +31,11 @@ declare module '@lightningjs/blits' {
31
31
  * Does not interupt the message when it's already being announced.
32
32
  */
33
33
  cancel()
34
+ /**
35
+ * Interrupts a specific message as it is being spoken out by the Text to Speech
36
+ * engine.
37
+ */
38
+ stop()
34
39
  }
35
40
 
36
41
  export interface Announcer {
@@ -57,7 +62,7 @@ declare module '@lightningjs/blits' {
57
62
  /**
58
63
  * Clears out the announcement queue of messages.
59
64
  */
60
- clears(): void;
65
+ clear(): void;
61
66
  /**
62
67
  * Enables the announcer.
63
68
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.26.1",
3
+ "version": "1.27.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -18,7 +18,8 @@
18
18
  "lint": "eslint '**/*.js'",
19
19
  "lint:fix": "eslint '**/*.js' --fix",
20
20
  "prepublishOnly": "node scripts/prepublishOnly.js",
21
- "postpublish": "node scripts/postpublish.js"
21
+ "postpublish": "node scripts/postpublish.js",
22
+ "prepare": "husky"
22
23
  },
23
24
  "types": "./index.d.ts",
24
25
  "lint-staged": {
@@ -26,26 +27,20 @@
26
27
  "eslint --fix"
27
28
  ]
28
29
  },
29
- "husky": {
30
- "hooks": {
31
- "pre-commit": "lint-staged"
32
- }
33
- },
34
30
  "type": "module",
35
31
  "author": "Michiel van der Geest <hello@michielvandergeest.com>",
36
32
  "license": "Apache-2.0",
37
33
  "devDependencies": {
38
34
  "@babel/eslint-parser": "^7.26.5",
39
35
  "@babel/plugin-syntax-import-assertions": "^7.26.0",
40
- "babel-eslint": "^10.1.0",
41
36
  "c8": "^8.0.1",
42
37
  "eslint": "^8.8.0",
43
38
  "eslint-config-prettier": "^8.3.0",
44
39
  "eslint-plugin-prettier": "^4.0.0",
45
40
  "global-jsdom": "24.0.0",
46
- "husky": "^7.0.4",
41
+ "husky": "^9.1.7",
47
42
  "jsdom": "24.0.0",
48
- "lint-staged": "^12.3.3",
43
+ "lint-staged": "^15.5.0",
49
44
  "prettier": "^2.5.1",
50
45
  "tap-diff": "^0.1.1",
51
46
  "tape": "^5.5.0"
@@ -0,0 +1,78 @@
1
+ /*
2
+ * If not stated otherwise in this file or this component's LICENSE file the
3
+ * following copyright and licenses apply:
4
+ *
5
+ * Copyright 2023 Comcast
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the License);
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import { execSync } from 'child_process'
21
+ import { readFileSync } from 'fs'
22
+
23
+ // Read the input provided by the pre-push hook
24
+ const input = readFileSync(0, 'utf-8').trim().split(' ')
25
+ if (input.length < 4) {
26
+ console.log('Insufficient input from pre-push hook. Skipping lint.')
27
+ process.exit(0)
28
+ }
29
+
30
+ const localSha = input[1] // The SHA being pushed
31
+ const remoteSha = input[3] // The remote SHA, can be zeros for new branch
32
+
33
+ // If remote is all zeros, it's a new branch
34
+ const isNewBranch = remoteSha === '0000000000000000000000000000000000000000'
35
+
36
+ let filesToLint
37
+
38
+ try {
39
+ if (isNewBranch) {
40
+ // For a new branch, compare with dev
41
+ filesToLint = execSync(`git diff --name-only dev...${localSha} | grep '\\.js$'`, {
42
+ encoding: 'utf8',
43
+ })
44
+ } else {
45
+ // Only check files in the commits that are being pushed right now
46
+ filesToLint = execSync(`git diff --name-only ${remoteSha}..${localSha} | grep '\\.js$'`, {
47
+ encoding: 'utf8',
48
+ })
49
+ }
50
+ } catch (error) {
51
+ // grep will exit with code 1 if it doesn't find anything
52
+ console.log('No JavaScript files changed. Nothing to lint.')
53
+ process.exit(0)
54
+ }
55
+
56
+ if (!filesToLint.trim()) {
57
+ console.log('No JavaScript files changed. Nothing to lint.')
58
+ process.exit(0)
59
+ }
60
+
61
+ const fileList = filesToLint.trim().split('\n')
62
+ console.log(`Running ESLint on ${fileList.length} changed JavaScript files:\n`)
63
+ console.log(filesToLint)
64
+
65
+ try {
66
+ // Run ESLint on the changed files
67
+ execSync(`npx eslint ${fileList.join(' ')}`, { stdio: 'inherit' })
68
+ console.log('Linting passed successfully!\n')
69
+ process.exit(0)
70
+ } catch (error) {
71
+ console.log('\n==========================================================================')
72
+ console.log('ESLint found issues in your code. Please fix them before pushing.')
73
+ console.log('You can run:\n')
74
+ console.log('npx eslint --fix "**/*.js"\n')
75
+ console.log('on the specific files to automatically fix some issues.')
76
+ console.log('==========================================================================\n')
77
+ process.exit(1)
78
+ }
@@ -21,6 +21,15 @@ let active = false
21
21
  let count = 0
22
22
  const queue = []
23
23
  let isProcessing = false
24
+ let currentId = null
25
+ let debounce = null
26
+
27
+ const noopAnnouncement = {
28
+ then() {},
29
+ done() {},
30
+ cancel() {},
31
+ stop() {},
32
+ }
24
33
 
25
34
  const enable = () => {
26
35
  active = true
@@ -35,13 +44,13 @@ const toggle = (v) => {
35
44
  }
36
45
 
37
46
  const speak = (message, politeness = 'off') => {
38
- if (active === false) return
47
+ if (active === false) return noopAnnouncement
39
48
 
40
49
  return addToQueue(message, politeness)
41
50
  }
42
51
 
43
52
  const pause = (delay) => {
44
- if (active === false) return
53
+ if (active === false) return noopAnnouncement
45
54
 
46
55
  return addToQueue(undefined, undefined, delay)
47
56
  }
@@ -64,6 +73,15 @@ const addToQueue = (message, politeness, delay = false) => {
64
73
  resolveFn('canceled')
65
74
  }
66
75
 
76
+ // augment the promise with a stop function
77
+ done.stop = () => {
78
+ if (id === currentId) {
79
+ speechSynthesis.cancel()
80
+ isProcessing = false
81
+ resolveFn('interupted')
82
+ }
83
+ }
84
+
67
85
  // add message of pause
68
86
  if (delay === false) {
69
87
  politeness === 'assertive'
@@ -84,26 +102,37 @@ const processQueue = async () => {
84
102
  if (isProcessing === true || queue.length === 0) return
85
103
  isProcessing = true
86
104
 
87
- const { message, resolveFn, delay } = queue.shift()
105
+ const { message, resolveFn, delay, id } = queue.shift()
106
+
107
+ currentId = id
88
108
 
89
- if (delay !== undefined) {
109
+ if (delay) {
90
110
  setTimeout(() => {
91
111
  isProcessing = false
112
+ currentId = null
92
113
  resolveFn('finished')
93
114
  processQueue()
94
115
  }, delay)
95
116
  } else {
96
- speechSynthesis
97
- .speak({ message })
98
- .then(() => {
99
- isProcessing = false
100
- resolveFn('finished')
101
- processQueue()
102
- })
103
- .catch((e) => {
104
- isProcessing = false
105
- resolveFn(e.error)
106
- })
117
+ if (debounce !== null) clearTimeout(debounce)
118
+ // add some easing when speaking the messages to reduce stuttering
119
+ debounce = setTimeout(() => {
120
+ speechSynthesis
121
+ .speak({ message })
122
+ .then(() => {
123
+ isProcessing = false
124
+ currentId = null
125
+ resolveFn('finished')
126
+ processQueue()
127
+ })
128
+ .catch((e) => {
129
+ isProcessing = false
130
+ currentId = null
131
+ resolveFn(e.error)
132
+ processQueue()
133
+ })
134
+ debounce = null
135
+ }, 200)
107
136
  }
108
137
  }
109
138
 
@@ -35,7 +35,6 @@ export default () =>
35
35
  hashchangeHandler = () => Router.navigate.apply(this)
36
36
  Router.navigate.apply(this)
37
37
  window.addEventListener('hashchange', hashchangeHandler)
38
-
39
38
  },
40
39
  destroy() {
41
40
  window.removeEventListener('hashchange', hashchangeHandler, false)
@@ -170,7 +170,11 @@ test('deregisterListener - only deregisters specified component listener', (t) =
170
170
  eventListener.deregisterListener(componentA, event)
171
171
 
172
172
  const result = eventListener.executeListeners(event)
173
- t.equal(result, true, 'Should return true when listeners are still registered after deregistering one')
173
+ t.equal(
174
+ result,
175
+ true,
176
+ 'Should return true when listeners are still registered after deregistering one'
177
+ )
174
178
  t.equal(callCount1, 0, 'Should not call deregistered callback')
175
179
  t.equal(callCount2, 1, 'Should call remaining registered callback')
176
180