@mattduffy/banner 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/.eslintrc.cjs ADDED
@@ -0,0 +1,37 @@
1
+ module.exports = {
2
+ globals: {
3
+ window: true,
4
+ document: true,
5
+ origin: true,
6
+ worker: true,
7
+ },
8
+ env: {
9
+ es2021: true,
10
+ node: true,
11
+ browser: true,
12
+ },
13
+ plugins: [
14
+ ],
15
+ extends: 'airbnb-base',
16
+ overrides: [
17
+ {
18
+ files: ["public/j/worker.js"],
19
+ rules: {
20
+ 'no-restricted-globals': ['error', 'isFinite', 'isNaN'].concat(restrictedGlobals),
21
+ },
22
+ },
23
+ ],
24
+ parserOptions: {
25
+ ecmaVersion: 'latest',
26
+ sourceType: 'module',
27
+ },
28
+ rules: {
29
+ semi: ['error', 'never'],
30
+ 'no-console': 'off',
31
+ 'no-underscore-dangle': 'off',
32
+ 'import/extensions': 'off',
33
+ 'import/prefer-default-export': 'off',
34
+ 'max-len': ['error', {"code": 100}],
35
+ 'new-cap': 'off',
36
+ },
37
+ }
package/LICENSE.txt ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 Matthew Duffy <mattduffy@gmail.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,8 @@
1
+ # Banner
2
+ This package exports a middleware function for Koajs app servers that displays useful start-up
3
+ information.
4
+
5
+ ```javascript
6
+ import banner from '@mattduffy/banner'
7
+ app.use(banner)
8
+ ```
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@mattduffy/banner",
3
+ "version": "1.0.0",
4
+ "description": "Displays server start-up information.",
5
+ "author": "Matthew Duffy",
6
+ "license": "ISC",
7
+ "main": "index.js",
8
+ "type": "module",
9
+ "homepage": "https://github.com/mattduffy/banner#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mattduffy/banner.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/mattduffy/banner/issues"
16
+ },
17
+ "scripts": {
18
+ "test": "DEBUG=* node --test test/test.js"
19
+ },
20
+ "exports": {
21
+ ".": "./src/index.js",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "devDependencies": {
25
+ "eslint": "8.2.0",
26
+ "eslint-config-airbnb-base": "15.0.0",
27
+ "eslint-plugin-import": "2.25.2"
28
+ },
29
+ "keywords": [],
30
+ "dependencies": {
31
+ "debug": "4.4.1"
32
+ }
33
+ }
package/src/index.js ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @module @mattduffy/banner
3
+ * @author Matthew Duffy <mattduffy@gmail.com>
4
+ * @file src/index.js The Banner class definition file.
5
+ */
6
+
7
+ import Debug from 'debug'
8
+
9
+ Debug.log = console.log.bind(console)
10
+ const log = Debug('banner')
11
+ const error = log.extend('ERROR')
12
+
13
+ /**
14
+ * A class to create and emit a start-up banner of Koajs based apps.
15
+ * @summary Create and emit an app start-up banner.
16
+ * @class Banner
17
+ * @author Matthew Duffy <mattduffy@gmail.com>
18
+ */
19
+ export class Banner {
20
+ #appName
21
+ #arch
22
+ #bannerText
23
+ #borderGlyph = '#'
24
+ #lineStarts = []
25
+ #lines = []
26
+ #local
27
+ #localPort
28
+ #nodejs
29
+ #nodeLts
30
+ #nodeName
31
+ #nodeVersion
32
+ #platform
33
+ #public
34
+ #startingup
35
+ /**
36
+ * Create an instance of the Banner class.
37
+ * @param { object } [strings] - An object literal of strings to display in the banner.
38
+ * @param { string } strings.name - The name of the app starting up.
39
+ * @param { string } strings.public - The public web address the app is accesssible at.
40
+ * @param { string } strings.local - The local address the app is accessible at.
41
+ * @param { Number } [strings.localPort] - The local address port, if provided.
42
+ * @returns { Banner}
43
+ */
44
+ constructor(strings=null) {
45
+ this.#arch = process.arch
46
+ this.#nodeLts = process.release?.lts ?? null
47
+ this.#nodeName = process.release.name
48
+ this.#nodeVersion = process.version
49
+ this.#platform = process.platform
50
+
51
+ this.archLabel = 'arch'
52
+ this.localLabel = 'local'
53
+ this.nodejsLabel = 'process'
54
+ this.publicLabel = 'public'
55
+ this.startingupLabel = 'Starting up'
56
+
57
+ if (strings) {
58
+ this.#appName = strings.name
59
+ this.#borderGlyph = strings?.borderGlyph ?? this.#borderGlyph
60
+ this.#localPort = strings?.localPort ?? null
61
+ this.#local = `${strings.local}${(this.#localPort) ? ':' + this.#localPort : ''}`
62
+ this.#public = strings.public
63
+ this.#startingup = strings.name
64
+ }
65
+
66
+ if (this.#appName && this.#local && this.#public) {
67
+ this.#compose()
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Compose the inputs in to the banner text.
73
+ * @throws { Error } - Throws an exception if name, public, local values are missing.
74
+ * @return { void }
75
+ */
76
+ #compose () {
77
+ if (!this.#appName || !this.#local || !this.#public) {
78
+ throw new Error('Missing required inputs.')
79
+ }
80
+ this.startingupLine = `${this.startingupLabel}: ${this.#startingup}`
81
+ this.#lineStarts.push(this.startingupLine)
82
+ this.localLine = `${this.localLabel}: `
83
+ + `${/^https?:\/\//.test(this.#local) ? this.#local : 'http://' + this.#local}`
84
+ this.#lineStarts.push(this.localLine)
85
+ this.publicLine = `${this.publicLabel}: `
86
+ + `${/^https?:\/\//.test(this.#public) ? this.#public : 'https://' + this.#public}`
87
+ this.#lineStarts.push(this.publicLine)
88
+ this.archLine = `${this.archLabel}: ${this.#arch} ${this.#platform}`
89
+ this.#lineStarts.push(this.archLine)
90
+ this.nodejsLine = `${this.nodejsLabel}: ${this.#nodeName} ${this.#nodeVersion} `
91
+ + `${(this.#nodeLts) ? '(' + this.#nodeLts + ')' : ''}`
92
+ this.#lineStarts.push(this.nodejsLine)
93
+
94
+ this.startingupLine = this.startingupLine.padStart(
95
+ (this.longestLabel - this.startingupLine.indexOf(':'))
96
+ + this.startingupLine.length, ' ')
97
+ this.#lines[0] = this.startingupLine
98
+
99
+ this.localLine = this.localLine.padStart(
100
+ (this.longestLabel - this.localLine.indexOf(':'))
101
+ + this.localLine.length, ' ')
102
+ this.#lines[1] = this.localLine
103
+
104
+ this.publicLine = this.publicLine.padStart(
105
+ (this.longestLabel - this.publicLine.indexOf(':'))
106
+ + this.publicLine.length, ' ')
107
+ this.#lines[2] = this.publicLine
108
+
109
+ this.nodejsLine = this.nodejsLine.padStart(
110
+ (this.longestLabel - this.nodejsLine.indexOf(':'))
111
+ + this.nodejsLine.length, ' ')
112
+ this.#lines[3] = this.nodejsLine
113
+
114
+ this.archLine = this.archLine.padStart(
115
+ (this.longestLabel - this.archLine.indexOf(':'))
116
+ + this.archLine.length, ' ')
117
+ this.#lines[4] = this.archLine
118
+
119
+ const g = this.#borderGlyph
120
+ this.#bannerText =
121
+ `${g}${g.padEnd(this.longestLine + 5, g)}${g}\n`
122
+ + `${g} ${' '.padEnd(this.longestLine + 2, ' ')} ${g}\n`
123
+ + `${g} ${this.startingupLine}${' '.padEnd(
124
+ (this.longestLine - this.startingupLine.length) + 3, ' ')} ${g}\n`
125
+ + `${g} ${this.localLine}${' '.padEnd(
126
+ (this.longestLine - this.localLine.length) + 3, ' ')} ${g}\n`
127
+ + `${g} ${this.publicLine}${' '.padEnd(
128
+ (this.longestLine - this.publicLine.length) + 3, ' ')} ${g}\n`
129
+ + `${g} ${this.nodejsLine}${' '.padEnd(
130
+ (this.longestLine - this.nodejsLine.length) + 3, ' ')} ${g}\n`
131
+ + `${g} ${this.archLine}${' '.padEnd(
132
+ (this.longestLine - this.archLine.length) + 3, ' ')} ${g}\n`
133
+ + `${g} ${' '.padEnd(this.longestLine + 2, ' ')} ${g}\n`
134
+ + `${g}${g.padEnd(this.longestLine + 5, g)}${g}\n`
135
+ }
136
+
137
+ /**
138
+ * Emits the entire composed banner string through the console.log method.
139
+ * @type { Boolean }
140
+ * @return { Boolean } - True if #bannerText has content, otherwise false.
141
+ */
142
+ print() {
143
+ if (this.#bannerText) {
144
+ console.log(this.#bannerText)
145
+ return true
146
+ }
147
+ return false
148
+ }
149
+
150
+ /**
151
+ * Set the name of the app.
152
+ * @type { string }
153
+ * @param { string } name - The name of the app to display in the banner.
154
+ */
155
+ set name(name) {
156
+ this.#appName = name
157
+ this.#startingup = name
158
+ if (this.#local && this.#public) {
159
+ this.#compose()
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Set the local address displayed in the banner.
165
+ * @type { string }
166
+ * @param { string } local - The local address displayed in the banner.
167
+ */
168
+ set local(local) {
169
+ this.#local = `${local}${(this.#localPort) ? ':' + this.#localPort : ''}`
170
+ if (this.#appName && this.#public) {
171
+ this.#compose()
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Set the local address port displayed in the banner.
177
+ * @type { string }
178
+ * @param { string } port - The local address port displayed in the banner.
179
+ */
180
+ set localPort(port) {
181
+ this.#localPort = port
182
+ this.#local = `${this.#local}:${port}`
183
+ if (this.#appName && this.#public) {
184
+ this.#compose()
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Set the public address displayed in the banner.
190
+ * @type { string }
191
+ * @param { string } glyph - The public address displayed in the banner.
192
+ */
193
+ set public(pub) {
194
+ this.#public = pub
195
+ if (this.#appName && this.#local) {
196
+ this.#compose()
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Set the border glyph of the banner.
202
+ * @type { string }
203
+ * @param { string } glyph - The border glyph character of the banner.
204
+ */
205
+ set borderGlyph(glyph) {
206
+ this.#borderGlyph = glyph
207
+ if (this.#appName && this.#local && this.#public) {
208
+ this.#compose()
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Return the entire composed banner as a string.
214
+ * @type { string }
215
+ * @return { string } - The composed banner as a string.
216
+ */
217
+ get bannerText() {
218
+ return this.#bannerText
219
+ }
220
+
221
+ /**
222
+ * Return the length of the longest line label as an int.
223
+ * @type { number }
224
+ * @return { number } - Length of the longest line label.
225
+ */
226
+ get longestLabel() {
227
+ return this.#lineStarts.reduce((a, c) => {
228
+ if (a > (c.indexOf(':') + 1)) {
229
+ return a
230
+ }
231
+ return (c.indexOf(':') + 1)
232
+ }, '')
233
+ }
234
+
235
+ /**
236
+ * Return the length of the longest line of text as an int.
237
+ * @type { number }
238
+ * @return { number } - Length of the longest line of text.
239
+ */
240
+ get longestLine() {
241
+ return this.#lines.reduce((a, c) => {
242
+ if (a > c.length) {
243
+ return a
244
+ }
245
+ return c.length
246
+ }, '')
247
+ }
248
+
249
+ /**
250
+ * Create a middleware method that generates a banner for each request.
251
+ * @param { function } [log] - an optional reference to the app level logging function.
252
+ * @param { function } [error] - an optional reference to the app level error logging function.
253
+ * @returns { (ctx:object, next:function) => mixed } - Koa middleware function.
254
+ */
255
+ use(log=null, error=null) {
256
+ const _log = log ?? console.log
257
+ const _error = error ?? console.error
258
+ _log('adding request banner to the app.')
259
+ const g = this.#borderGlyph
260
+ const n = this.#appName
261
+ return async function banner(ctx, next){
262
+ try {
263
+ const _g = (/post/i.test(ctx.request.method)) ? '@' : g
264
+ const _urlLabel = `${ctx.request.method}:`
265
+ const _url = `${ctx.request.header.host}${ctx.request.url}`
266
+ const _urlLine = `${_urlLabel} ${_url}`
267
+ const _refLabel = 'Referer:'
268
+ const _ref = ctx.request.header.referer ?? '<emtpy header field>'
269
+ const _refLine = `${_refLabel} ${_ref}`
270
+ const _longestLabel = [_urlLabel, _refLabel].reduce((a, c) => {
271
+ if (a > (c.indexOf(':') + 1)) {
272
+ return a
273
+ }
274
+ return (c.indexOf(':' + 1))
275
+ }, '')
276
+ const _longestLine = [_urlLine, _refLine].reduce((a, c) => {
277
+ if (a > c.length) return a
278
+ return c.length
279
+ }, '')
280
+ // _log('request banner _longestLine', _longestLine)
281
+ const _requestBanner = `${_g.padEnd(_longestLine + 5, _g)}\n`
282
+ + `${_g} ${_urlLine}\n`
283
+ + `${_g} ${_refLine}\n`
284
+ + `${_g.padEnd(_longestLine + 5, _g)}`
285
+ _log(_requestBanner)
286
+ await next()
287
+ return true
288
+ } catch (e) {
289
+ _error('Failed after adding start-up banner.')
290
+ _error(e)
291
+ ctx.throw(500, 'Error after adding start-up banner.', e)
292
+ }
293
+ }
294
+ }
295
+ }
package/test/test.js ADDED
@@ -0,0 +1,88 @@
1
+ // import path from 'node:path'
2
+ // import { randomBytes } from 'node:crypto'
3
+ import {
4
+ after,
5
+ before,
6
+ describe,
7
+ it,
8
+ } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import fs from 'node:fs/promises'
11
+ import { Banner } from '../src/index.js'
12
+ const skip = { skip: true }
13
+ let cfg
14
+ let ctx
15
+ let ctx_POST
16
+ let ctx_GET
17
+ let next
18
+ console.log(skip)
19
+ describe('First test suite for banner package', async () => {
20
+ before(() => {
21
+ ctx = {
22
+ request: {
23
+ method: '',
24
+ url: '/a/really/long/url/to/a/special/page',
25
+ header: {
26
+ host: 'https://banner.test',
27
+ referer: 'https://googoogle.com',
28
+ },
29
+ },
30
+ throw: (code, msg) => {
31
+ throw new Error(`Error code ${code}: ${msg}`)
32
+ }
33
+ }
34
+ ctx_POST = { ...ctx }
35
+ ctx_GET = { ...ctx }
36
+ next = async () => {
37
+ setTimeout(() => {
38
+ console.log('the next() function')
39
+ }, 1000)
40
+ }
41
+ cfg = {
42
+ name: 'Banner Test #1',
43
+ public: 'https://banner.test',
44
+ local: 'http://banner.local',
45
+ localPort: 8921,
46
+ }
47
+ })
48
+
49
+ it('Should create a start-up banner instance with no constructor paramater given.', () => {
50
+ const banner = new Banner()
51
+ assert(banner instanceof Banner)
52
+ })
53
+
54
+ it('Should create a start-up banner instance, with constructor param.', () => {
55
+ const banner = new Banner(cfg)
56
+ assert(banner.bannerText.length > 0)
57
+ assert(typeof banner.bannerText === 'string')
58
+ })
59
+
60
+ it('Should fail', () => {
61
+ const banner = new Banner()
62
+ assert(!banner.print())
63
+ })
64
+
65
+ it('Should work as a koajs middleware function.', async () => {
66
+ ctx_POST = Object.assign(ctx_POST, ctx)
67
+ ctx_POST.request.method = 'POST'
68
+ console.log(ctx_POST)
69
+ const post = new Banner(ctx_POST)
70
+ assert(await post.use()(ctx, next))
71
+
72
+ ctx_GET = Object.assign(ctx_GET, ctx)
73
+ ctx_GET.request.method = 'GET'
74
+ console.log(ctx_GET)
75
+ const get = new Banner(ctx_GET)
76
+ assert(await get.use()(ctx, next))
77
+ })
78
+
79
+ it('Should fail as a koajs middleware function, missing input parameters.', async () => {
80
+ ctx.url= ''
81
+ ctx.request.header.host = undefined
82
+ ctx.request.url = undefined
83
+ console.log(ctx)
84
+ const post = new Banner()
85
+ await post.use()(ctx, next)
86
+ // assert.throws(await post.use()(ctx, next))
87
+ })
88
+ })