@caweb/a11y-webpack-plugin 1.0.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/README.md ADDED
File without changes
package/aceconfig.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Configuration for Accessibility Checker
3
+ * @link https://www.npmjs.com/package/accessibility-checker
4
+ */
5
+
6
+ let levels = [
7
+ 'violation',
8
+ 'potentialviolation',
9
+ 'recommendation',
10
+ 'potentialrecommendation',
11
+ 'manual',
12
+ 'pass'
13
+ ];
14
+ let reportLevels = levels;
15
+ let failLevels = levels;
16
+
17
+ // process args
18
+ process.argv.forEach((arg) => {
19
+ // remove any report levels
20
+ if( arg.includes('--no-report-levels-') ){
21
+ let r = arg.replace('--no-report-levels-', '')
22
+ delete reportLevels[reportLevels.indexOf(r)]
23
+ }
24
+ // remove any fails levels
25
+ if( arg.includes('--no-fail-levels-') ){
26
+ let f = arg.replace('--no-fail-levels-', '')
27
+ delete failLevels[failLevels.indexOf(f)]
28
+ }
29
+ })
30
+
31
+ export default {
32
+ ruleArchive: "latest",
33
+ policies: [
34
+ 'WCAG_2_1'
35
+ ],
36
+ failLevels: failLevels.filter(e=>e),
37
+ reportLevels: reportLevels.filter(e=>e),
38
+ outputFilename: 'a11y',
39
+ outputFolder: "public",
40
+ outputFormat: [
41
+ 'html'
42
+ ],
43
+ outputFilenameTimestamp: false
44
+ }
package/index.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External dependencies
5
+ */
6
+ import { sync as resolveBin } from 'resolve-bin';
7
+ import spawn from 'cross-spawn';
8
+ import { getAllFilesSync } from 'get-all-files'
9
+ import EntryDependency from "webpack/lib/dependencies/EntryDependency.js";
10
+ import path from 'path';
11
+ import { isUrl, isValidUrl } from 'check-valid-url';
12
+ import fs from 'fs';
13
+ import deepmerge from 'deepmerge';
14
+ import chalk from 'chalk';
15
+ import { fileURLToPath, URL } from 'url';
16
+
17
+ // default configuration
18
+ import {default as DefaultConfig} from './aceconfig.js';
19
+
20
+ const boldWhite = chalk.bold.white;
21
+ const boldGreen = chalk.bold.green;
22
+ const boldBlue = chalk.bold.hex('#03a7fc');
23
+ const currentPath = path.dirname(fileURLToPath(import.meta.url));
24
+
25
+ // IBM Accessibility Checker Plugin
26
+ class A11yPlugin {
27
+ config = {}
28
+
29
+ constructor(opts = {}) {
30
+ // outputFolder must be resolved
31
+ if( opts.outputFolder ){
32
+ opts.outputFolder = path.join(process.cwd(), opts.outputFolder);
33
+ }
34
+ this.config = deepmerge(
35
+ DefaultConfig,
36
+ {
37
+ outputFolder: path.join(currentPath, DefaultConfig.outputFolder)
38
+ },
39
+ opts
40
+ );
41
+ }
42
+
43
+ apply(compiler) {
44
+ const staticDir = {
45
+ directory: this.config.outputFolder,
46
+ watch: true
47
+ }
48
+
49
+ let { devServer, output } = compiler.options;
50
+ let hostUrl = 'localhost' === devServer.host ? `http://${devServer.host}`: devServer.host;
51
+ let hostPort = devServer.port;
52
+
53
+ if( hostPort && 80 !== hostPort )
54
+ {
55
+ hostUrl = `${hostUrl}:${hostPort}`;
56
+ }
57
+
58
+ // if dev server allows for multiple pages to be opened
59
+ // add outputFilename.html to open property.
60
+ if( Array.isArray(devServer.open) ){
61
+ devServer.open.push(`${hostUrl}/${this.config.outputFilename}.html`)
62
+ }else if( 'object' === typeof devServer.open && Array.isArray(devServer.open.target) ){
63
+ devServer.open.target.push(`${hostUrl}/${this.config.outputFilename}.html`)
64
+ }
65
+
66
+ // add our static directory
67
+ if( Array.isArray(devServer.static) ){
68
+ devServer.static.push(staticDir)
69
+ }else{
70
+ devServer.static = [].concat(devServer.static, staticDir );
71
+ }
72
+
73
+ // Wait for configuration preset plugins to apply all configure webpack defaults
74
+ compiler.hooks.initialize.tap('IBM Accessibility Plugin', () => {
75
+ compiler.hooks.compilation.tap(
76
+ "IBM Accessibility Plugin",
77
+ (compilation, { normalModuleFactory }) => {
78
+ compilation.dependencyFactories.set(
79
+ EntryDependency,
80
+ normalModuleFactory
81
+ );
82
+ }
83
+ );
84
+
85
+ const { entry, options, context } = {
86
+ entry: path.join( this.config.outputFolder, 'a11y.update.js'),
87
+ options: {
88
+ name: 'a11y'
89
+ },
90
+ context: 'a11y'
91
+ };
92
+
93
+ const dep = new EntryDependency(entry);
94
+ dep.loc = {
95
+ name: options.name
96
+ };
97
+ if( ! fs.existsSync(path.resolve(this.config.outputFolder))){
98
+ fs.mkdirSync( path.resolve(this.config.outputFolder), {recursive: true} );
99
+ }
100
+
101
+ fs.writeFileSync(
102
+ path.join(this.config.outputFolder, `a11y.update.js`),
103
+ `` // required for hot-update to compile on our page, blank script for now
104
+ );
105
+
106
+
107
+ compiler.hooks.thisCompilation.tap('IBM Accessibility Plugin',
108
+ /**
109
+ * Hook into the webpack compilation
110
+ * @param {Compilation} compilation
111
+ */
112
+ (compilation) => {
113
+
114
+ compiler.hooks.make.tapAsync("IBM Accessibility Plugin", (compilation, callback) => {
115
+
116
+ compilation.addEntry(
117
+ context,
118
+ dep,
119
+ options,
120
+ err => {
121
+ callback(err);
122
+ });
123
+ });
124
+
125
+ compiler.hooks.done.tapAsync(
126
+ 'IBM Accessibility Plugin',
127
+ /**
128
+ * Hook into the process assets hook
129
+ * @param {any} _
130
+ * @param {(err?: Error) => void} callback
131
+ */
132
+ ({compilation}, callback) => {
133
+
134
+ console.log(`<i> ${boldGreen('[webpack-dev-middleware] Running IBM Accessibility scan...')}`);
135
+
136
+ this.a11yCheck(path.join(process.cwd(), output.publicPath ?? '/' ), this.config );
137
+
138
+ console.log(`<i> ${boldGreen('[webpack-dev-middleware] IBM Accessibilty Report can be viewed at')} ${ boldBlue(new URL(`${hostUrl}/${this.config.outputFilename}.html`).toString()) }`);
139
+
140
+ callback();
141
+ });
142
+
143
+ compiler.hooks.watchClose.tap( 'IBM Accessibility Plugin', () => {
144
+ getAllFilesSync(compiler.options.output.path).toArray().forEach(f => {
145
+ if(
146
+ f.includes('a11y') || // delete any a11y files
147
+ f.includes('.hot-update.js') // delete any HMR files
148
+ ){
149
+ fs.rmSync(f)
150
+ }
151
+ })
152
+ });
153
+
154
+ });
155
+
156
+ });
157
+
158
+ }
159
+
160
+ /**
161
+ * Run accessibility checks
162
+ *
163
+ * @param {Object} options
164
+ * @param {boolean} options.debug True if debug mode is enabled.
165
+ * @param {boolean} options.ruleArchive Specify the rule archive.
166
+ * @param {boolean} options.policies Specify one or many policies to scan.
167
+ * @param {boolean} options.failLevels Specify one or many violation levels on which to fail the test.
168
+ * @param {boolean} options.reportLevels Specify one or many violation levels that should be reported.
169
+ * @param {boolean} options.labels Specify labels that you would like associated to your scan.
170
+ * @param {boolean} options.outputFormat In which formats should the results be output.
171
+ * @param {boolean} options.outputFilename Filename for the scan results.
172
+ * @param {boolean} options.outputFolder Where the scan results should be saved.
173
+ * @param {boolean} options.outputFilenameTimestamp Should the timestamp be included in the filename of the reports?
174
+ */
175
+ a11yCheck(url, {
176
+ debug,
177
+ ruleArchive,
178
+ policies,
179
+ failLevels,
180
+ reportLevels,
181
+ labels,
182
+ outputFormat,
183
+ outputFilename,
184
+ outputFolder,
185
+ outputFilenameTimestamp
186
+ }){
187
+
188
+ let acheckerArgs = [
189
+ '--ruleArchive',
190
+ ruleArchive,
191
+ '--policies',
192
+ Array.isArray(policies) ? policies.filter(e => e).join(',') : policies,
193
+ '--failLevels',
194
+ Array.isArray(failLevels) ? failLevels.filter(e => e).join(',') : failLevels,
195
+ '--reportLevels',
196
+ Array.isArray(reportLevels) ? reportLevels.filter(e => e).join(',') : reportLevels,
197
+ '--outputFolder',
198
+ outputFolder,
199
+ '--outputFormat',
200
+ outputFormat,
201
+ '---outputFilenameTimestamp',
202
+ outputFilenameTimestamp,
203
+ url
204
+ ];
205
+
206
+ let isValid = false;
207
+
208
+ if( fs.existsSync( url ) ){
209
+ if( fs.statSync(url).isDirectory() && path.join( url, 'index.html') ){
210
+ url = path.join( url, 'index.html')
211
+ }
212
+ isValid = true;
213
+ }else{
214
+ isValid = 'localhost' === new URL(url).hostname || isUrl( url )
215
+ }
216
+
217
+ if( isValid ){
218
+ let originalFileName = `${fs.existsSync( url ) ?
219
+ path.resolve(url).replace(':', '_') :
220
+ url.replace(/http[s]+:\/\//, '')}.html`;
221
+ let originalJsonFileName = `${fs.existsSync( url ) ?
222
+ path.resolve(url).replace(':', '_') :
223
+ url.replace(/http[s]+:\/\//, '')}.json`;
224
+
225
+ let outputDir = path.resolve('.', outputFolder );
226
+
227
+ let {stderr, stdout} = spawn.sync(
228
+ resolveBin('accessibility-checker', {executable: 'achecker'}),
229
+ acheckerArgs,
230
+ {
231
+ stdio: 'pipe',
232
+ timeout: 30000 // stop after 30 seconds
233
+ }
234
+ )
235
+
236
+ if( stderr && stderr.toString() ){
237
+ console.log( stderr.toString() );
238
+ }
239
+
240
+ if( stdout && stdout.toString()){
241
+ let reportedFile = path.join(outputDir, originalFileName );
242
+ let reportedJSon = path.join(outputDir, originalJsonFileName );
243
+
244
+ // if output file name option was passed
245
+ if( outputFilename ){
246
+
247
+ reportedFile = path.join( outputDir, `${outputFilename}.html` );
248
+ reportedJSon = path.join( outputDir, `${outputFilename}.json` );
249
+
250
+ // rename the output files
251
+ fs.renameSync(path.join(outputDir, originalFileName), reportedFile );
252
+ fs.renameSync(path.join(outputDir, originalJsonFileName), reportedJSon );
253
+
254
+ // delete any empty directories.
255
+ fs.rmSync( path.join(outputDir, originalFileName.split(path.sep).shift()), {recursive: true} )
256
+ }
257
+
258
+ if( 'a11y' === process.argv[2] ){
259
+ console.log( reportedFile )
260
+ }else{
261
+ return reportedFile;
262
+ }
263
+ }
264
+ }else{
265
+ console.log( `${url} is not a valid url.` )
266
+ }
267
+
268
+ } // end of a11yCheck
269
+
270
+ } // end of class
271
+
272
+
273
+ export default A11yPlugin;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@caweb/a11y-webpack-plugin",
3
+ "version": "1.0.0",
4
+ "description": "CAWebPublishing Webpack Plugin to run Accessibility Scans",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "files": [
8
+ "aceconfig.js",
9
+ "index.js"
10
+ ],
11
+ "scripts": {
12
+ "test": "echo \"Error: run tests from root\" && exit 0"
13
+ },
14
+ "author": "CAWebPublishing",
15
+ "license": "ISC",
16
+ "bugs": {
17
+ "url": "https://github.com/CAWebPublishing/webpack/issues"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "homepage": "https://github.com/CAWebPublishing/webpack#readme",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/CAWebPublishing/webpack",
26
+ "directory": "plugins/a11y"
27
+ },
28
+ "keywords": [
29
+ "caweb",
30
+ "cagov",
31
+ "webpack"
32
+ ],
33
+ "dependencies": {
34
+ "accessibility-checker": "^3.1.73",
35
+ "check-valid-url": "^0.1.0"
36
+ }
37
+ }