@lightningjs/blits 1.26.2 → 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 +0 -1
- package/.github/workflows/comment-lint-results.yml +151 -0
- package/.github/workflows/comment-test-results.yml +114 -0
- package/.github/workflows/run-blits-tests.yml +95 -36
- package/.github/workflows/run-lint.yml +118 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/CHANGELOG.md +9 -0
- package/docs/plugins/text-to-speech-announcer.md +5 -1
- package/index.d.ts +6 -1
- package/package.json +5 -10
- package/scripts/prepush.js +78 -0
- package/src/announcer/announcer.js +43 -14
- package/src/components/RouterView.orig.js +0 -1
- package/src/lib/eventListeners.test.js +5 -1
package/.eslintrc.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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:
|
|
34
|
-
id:
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const results = {
|
|
41
|
+
timestamp,
|
|
42
|
+
summary: '',
|
|
43
|
+
testsFailed: false,
|
|
44
|
+
error: null
|
|
45
|
+
};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
54
|
+
// Read test report
|
|
55
|
+
const testOutput = fs.readFileSync('test-report.txt', 'utf8');
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
package/.husky/pre-push
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node ./scripts/prepush.js
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
14
|
## v1.26.1 / v1.26.2
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
41
|
+
"husky": "^9.1.7",
|
|
47
42
|
"jsdom": "24.0.0",
|
|
48
|
-
"lint-staged": "^
|
|
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
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
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
|
|