adapt-authoring-lang 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +6 -0
- package/bin/check.js +93 -0
- package/conf/config.schema.json +11 -0
- package/errors/errors.json +9 -0
- package/index.js +5 -0
- package/lib/LangModule.js +190 -0
- package/package.json +20 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a bug report
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to fill out this bug report!
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: What happened?
|
|
13
|
+
description: Please describe the issue
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: expected
|
|
18
|
+
attributes:
|
|
19
|
+
label: Expected behaviour
|
|
20
|
+
description: Tell us what should have happened
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: repro-steps
|
|
23
|
+
attributes:
|
|
24
|
+
label: Steps to reproduce
|
|
25
|
+
description: Tell us how to reproduce the issue
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: aat-version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Authoring tool version
|
|
32
|
+
description: What version of the Adapt authoring tool are you running?
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: input
|
|
36
|
+
id: fw-version
|
|
37
|
+
attributes:
|
|
38
|
+
label: Framework version
|
|
39
|
+
description: What version of the Adapt framework are you running?
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: browsers
|
|
42
|
+
attributes:
|
|
43
|
+
label: What browsers are you seeing the problem on?
|
|
44
|
+
multiple: true
|
|
45
|
+
options:
|
|
46
|
+
- Firefox
|
|
47
|
+
- Chrome
|
|
48
|
+
- Safari
|
|
49
|
+
- Microsoft Edge
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: logs
|
|
52
|
+
attributes:
|
|
53
|
+
label: Relevant log output
|
|
54
|
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
55
|
+
render: sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Request a new feature
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to request a new feature in the Adapt authoring tool! The Adapt team will consider all new feature requests, but unfortunately cannot commit to every one.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: Feature description
|
|
13
|
+
description: Please describe your feature request
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: checkboxes
|
|
17
|
+
id: contribute
|
|
18
|
+
attributes:
|
|
19
|
+
label: Can you work on this feature?
|
|
20
|
+
description: If you are able to commit your own time to work on this feature, it will greatly increase the liklihood of it being implemented by the core dev team. Otherwise, it will be triaged and prioritised alongside the core team's current priorities.
|
|
21
|
+
options:
|
|
22
|
+
- label: I can contribute
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[//]: # (Please title your PR according to eslint commit conventions)
|
|
2
|
+
[//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
|
|
3
|
+
|
|
4
|
+
[//]: # (Add a link to the original issue)
|
|
5
|
+
|
|
6
|
+
[//]: # (Delete as appropriate)
|
|
7
|
+
### Fix
|
|
8
|
+
* A sentence describing each fix
|
|
9
|
+
|
|
10
|
+
### Update
|
|
11
|
+
* A sentence describing each udpate
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
* A sentence describing each new feature
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* A sentence describing each breaking change
|
|
18
|
+
|
|
19
|
+
[//]: # (List appropriate steps for testing if needed)
|
|
20
|
+
### Testing
|
|
21
|
+
1. Steps for testing
|
|
22
|
+
|
|
23
|
+
[//]: # (Mention any other dependencies)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Add labelled PRs to project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [ labeled ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
add-to-project:
|
|
9
|
+
if: ${{ github.event.label.name == 'dependencies' }}
|
|
10
|
+
name: Add to main project
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/add-to-project@v0.1.0
|
|
14
|
+
with:
|
|
15
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
16
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Add to main project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
pull_request:
|
|
8
|
+
types:
|
|
9
|
+
- opened
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
add-to-project:
|
|
13
|
+
name: Add to main project
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/add-to-project@v0.1.0
|
|
17
|
+
with:
|
|
18
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
19
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
package/bin/check.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Checks for unused and missing language strings
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs/promises'
|
|
6
|
+
import { glob } from 'glob'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
|
|
9
|
+
const root = `${process.cwd().replaceAll(path.sep, '/')}/node_modules`
|
|
10
|
+
|
|
11
|
+
async function check () {
|
|
12
|
+
console.log('Checking for unused language strings')
|
|
13
|
+
|
|
14
|
+
const translatedStrings = await getTranslatedStrings()
|
|
15
|
+
const usedStrings = await getUsedStrings(translatedStrings)
|
|
16
|
+
|
|
17
|
+
logUnusedStrings(translatedStrings)
|
|
18
|
+
logMissingStrings(translatedStrings, usedStrings)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getTranslatedStrings () {
|
|
22
|
+
const langPacks = await glob(`${root}/adapt-authoring-langpack-*/lang`)
|
|
23
|
+
const keyMap = {}
|
|
24
|
+
await Promise.all((langPacks).map(async l => {
|
|
25
|
+
await Promise.all((await fs.readdir(l)).map(async f => {
|
|
26
|
+
const keys = JSON.parse(await fs.readFile(`${l}/${f}`))
|
|
27
|
+
const id = f.split('.')[1]
|
|
28
|
+
Object.keys(keys)
|
|
29
|
+
.map(k => `${id}.${k}`)
|
|
30
|
+
.forEach(k => {
|
|
31
|
+
keyMap[k] = false
|
|
32
|
+
})
|
|
33
|
+
}))
|
|
34
|
+
}))
|
|
35
|
+
return keyMap
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getUsedStrings (translatedStrings) {
|
|
39
|
+
const translatedKeys = Object.keys(translatedStrings)
|
|
40
|
+
const usedStrings = {}
|
|
41
|
+
const errorFiles = await glob(`${root}/adapt-authoring-*/errors/*.json`, { absolute: true })
|
|
42
|
+
await Promise.all(errorFiles.map(async f => {
|
|
43
|
+
Object.keys(JSON.parse((await fs.readFile(f)))).forEach(e => {
|
|
44
|
+
const key = `error.${e}`
|
|
45
|
+
if (!usedStrings[key]) usedStrings[key] = new Set()
|
|
46
|
+
usedStrings[key].add(f.replace(root, '').split('/')[1]) // only add module name for errors
|
|
47
|
+
})
|
|
48
|
+
}))
|
|
49
|
+
const sourceFiles = await glob(`${root}/adapt-authoring-*/**/*.@(js|hbs)`, { absolute: true })
|
|
50
|
+
await Promise.all(sourceFiles.map(async f => {
|
|
51
|
+
const contents = (await fs.readFile(f)).toString()
|
|
52
|
+
translatedKeys.forEach(k => {
|
|
53
|
+
translatedStrings[k] = contents.includes(k) ? true : undefined
|
|
54
|
+
})
|
|
55
|
+
const match = contents.matchAll(/['|"|`|](app\.[\w|\.]+)\W/g)
|
|
56
|
+
if (match) {
|
|
57
|
+
for (const m of match) {
|
|
58
|
+
const key = m[1]
|
|
59
|
+
if (!usedStrings[key]) usedStrings[key] = new Set()
|
|
60
|
+
usedStrings[key].add(f.replace(root, ''))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
return usedStrings
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function logUnusedStrings (data) {
|
|
68
|
+
const unusedKeys = Object.entries(data).filter(([k, v]) => !k.startsWith('error.') && !v).map(([k]) => k)
|
|
69
|
+
console.log('')
|
|
70
|
+
if (unusedKeys.length) {
|
|
71
|
+
unusedKeys.forEach(k => console.log(`- ${k}`))
|
|
72
|
+
console.log(`\n${unusedKeys.length} unused language strings found`)
|
|
73
|
+
process.exitCode = 1
|
|
74
|
+
} else {
|
|
75
|
+
console.log('\nNo unused strings!')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logMissingStrings (translatedStrings, usedStrings) {
|
|
80
|
+
console.log('')
|
|
81
|
+
const translatedKeys = Object.keys(translatedStrings)
|
|
82
|
+
const missingStrings = Object.entries(usedStrings).filter(([key]) => !translatedKeys.includes(key) && key !== 'app.js')
|
|
83
|
+
if (missingStrings.length) {
|
|
84
|
+
const sep = '\n => '
|
|
85
|
+
missingStrings.forEach(([key, files]) => console.log(`- ${key}${sep}${Array.from(files).join(sep)}`))
|
|
86
|
+
console.log(`\n${missingStrings.length} missing language strings found`)
|
|
87
|
+
process.exitCode = 1
|
|
88
|
+
} else {
|
|
89
|
+
console.log('\nNo missing strings!')
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
check()
|
package/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { AbstractModule } from 'adapt-authoring-core'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import { glob } from 'glob'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module to handle localisation of language strings
|
|
8
|
+
* @memberof lang
|
|
9
|
+
* @extends {AbstractModule}
|
|
10
|
+
*/
|
|
11
|
+
class LangModule extends AbstractModule {
|
|
12
|
+
/** @override */
|
|
13
|
+
async init () {
|
|
14
|
+
this.app.lang = this
|
|
15
|
+
await this.loadPhrases()
|
|
16
|
+
this.loadRoutes()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the languages supported by the application
|
|
21
|
+
* @type {Array<String>}
|
|
22
|
+
*/
|
|
23
|
+
get supportedLanguages () {
|
|
24
|
+
return Object.keys(this.phrases)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Loads, validates and merges all defined langage phrases
|
|
29
|
+
* @return {Promise}
|
|
30
|
+
*/
|
|
31
|
+
async loadPhrases () {
|
|
32
|
+
/**
|
|
33
|
+
* The loaded language phrases to be used for translation
|
|
34
|
+
* @type {Object}
|
|
35
|
+
*/
|
|
36
|
+
this.phrases = {}
|
|
37
|
+
const deps = [
|
|
38
|
+
{ name: this.app.name, rootDir: process.cwd() },
|
|
39
|
+
...Object.values(this.app.dependencies)
|
|
40
|
+
]
|
|
41
|
+
return Promise.all(deps.map(async d => this.loadPhrasesForDir(d.rootDir)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load all lang phrases for a given directory
|
|
46
|
+
* @param {String} dir Directory to search
|
|
47
|
+
* @return {Promise} Resolves with the phrases
|
|
48
|
+
*/
|
|
49
|
+
async loadPhrasesForDir (dir) {
|
|
50
|
+
const files = await glob('lang/*.json', { cwd: dir, absolute: true })
|
|
51
|
+
await Promise.all(files.map(async f => {
|
|
52
|
+
try {
|
|
53
|
+
const contents = JSON.parse((await fs.readFile(f)).toString())
|
|
54
|
+
Object.entries(contents).forEach(([k, v]) => this.storeStrings(`${path.basename(f).replace('.json', '')}.${k}`, v))
|
|
55
|
+
} catch (e) {
|
|
56
|
+
this.log('error', e.message, f)
|
|
57
|
+
}
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
storeStrings (key, value) {
|
|
62
|
+
const i = key.indexOf('.')
|
|
63
|
+
const lang = key.slice(0, i)
|
|
64
|
+
if (!this.phrases[lang]) this.phrases[lang] = {}
|
|
65
|
+
this.phrases[lang][key.slice(i + 1)] = value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Loads the router & routes
|
|
70
|
+
* @return {Promise}
|
|
71
|
+
*/
|
|
72
|
+
async loadRoutes () {
|
|
73
|
+
const [auth, server] = await this.app.waitForModule('auth', 'server')
|
|
74
|
+
|
|
75
|
+
server.api.addMiddleware(this.addTranslationUtils.bind(this))
|
|
76
|
+
|
|
77
|
+
const router = server.api.createChildRouter('lang')
|
|
78
|
+
router.addRoute({
|
|
79
|
+
route: '/{:lang}',
|
|
80
|
+
handlers: { get: this.requestHandler.bind(this) },
|
|
81
|
+
meta: {
|
|
82
|
+
get: {
|
|
83
|
+
summary: 'Retrieve lang strings for single locale',
|
|
84
|
+
responses: {
|
|
85
|
+
200: {
|
|
86
|
+
description: 'Lang strings for the specified locale',
|
|
87
|
+
content: {
|
|
88
|
+
'application/json': {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
auth.unsecureRoute(router.path, 'get')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load all lang phrases for a language
|
|
100
|
+
* @param {String} lang The language of strings to load
|
|
101
|
+
* @return {Object} The phrases
|
|
102
|
+
*/
|
|
103
|
+
getPhrasesForLang (lang) {
|
|
104
|
+
const phrases = {}
|
|
105
|
+
Object.entries(this.phrases).forEach(([key, value]) => {
|
|
106
|
+
const i = key.indexOf('.')
|
|
107
|
+
const keyLang = key.slice(0, i)
|
|
108
|
+
const newKey = key.slice(i + 1)
|
|
109
|
+
if (keyLang === lang) phrases[newKey] = value
|
|
110
|
+
})
|
|
111
|
+
return Object.keys(phrases).length > 1 ? phrases : undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Adds a translate function to incoming API requests for generating language strings in the original request's supported language
|
|
116
|
+
* @param {external:ExpressRequest} req
|
|
117
|
+
* @param {external:ExpressResponse} res
|
|
118
|
+
* @param {function} next
|
|
119
|
+
*/
|
|
120
|
+
addTranslationUtils (req, res, next) {
|
|
121
|
+
const lang = req.acceptsLanguages(this.supportedLanguages)
|
|
122
|
+
req.translate = (key, data) => this.translate(lang, key, data)
|
|
123
|
+
next()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Shortcut to log a missing language key
|
|
128
|
+
* @param {external:ExpressRequest} req The client request object
|
|
129
|
+
* @param {external:ExpressResponse} res The server response object
|
|
130
|
+
* @param {Function} next The callback function
|
|
131
|
+
*/
|
|
132
|
+
requestHandler (req, res, next) {
|
|
133
|
+
// defaults to the request (browser) lang
|
|
134
|
+
const lang = req.params.lang || req.acceptsLanguages(this.supportedLanguages)
|
|
135
|
+
if (!lang || !this.phrases[lang]) {
|
|
136
|
+
return next(this.app.errors.UNKNOWN_LANG.setData({ lang }))
|
|
137
|
+
}
|
|
138
|
+
res.json(this.phrases[lang])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns translated language string
|
|
143
|
+
* @param {String} lang The target language (if undefined, the default server language will be used)
|
|
144
|
+
* @param {String|AdaptError} key The unique string key (if an AdaptError is passed, the error data will be used for the data param)
|
|
145
|
+
* @param {Object} data Dynamic data to be inserted into translated string
|
|
146
|
+
* @return {String}
|
|
147
|
+
*/
|
|
148
|
+
translate (lang, key, data) {
|
|
149
|
+
if (typeof lang !== 'string') {
|
|
150
|
+
lang = this.getConfig('defaultLang')
|
|
151
|
+
}
|
|
152
|
+
if (key.constructor.name === 'AdaptError') {
|
|
153
|
+
return this.translateError(lang, key)
|
|
154
|
+
}
|
|
155
|
+
const s = this.phrases[lang][key]
|
|
156
|
+
if (!s) {
|
|
157
|
+
this.log('warn', `missing key '${lang}.${key}'`)
|
|
158
|
+
return key
|
|
159
|
+
}
|
|
160
|
+
if (!data) {
|
|
161
|
+
return s
|
|
162
|
+
}
|
|
163
|
+
return Object.entries(data).reduce((s, [k, v]) => {
|
|
164
|
+
// map any errors specified in data
|
|
165
|
+
v = Array.isArray(v) ? v.map(v2 => this.translateError(lang, v2)) : this.translateError(lang, v)
|
|
166
|
+
s = s.replaceAll(`\${${k}}`, v)
|
|
167
|
+
// handle special-case array replacements
|
|
168
|
+
if (Array.isArray(v)) {
|
|
169
|
+
const matches = [...s.matchAll(new RegExp(String.raw`\$map{${k}:(.+)}`, 'g'))]
|
|
170
|
+
matches.forEach(([replace, data]) => {
|
|
171
|
+
const [attrs, delim] = data.split(':')
|
|
172
|
+
s = s.replace(replace, v.map(val => attrs.split(',').map(a => Object.prototype.hasOwnProperty.call(val, a) ? val[a] : a)).join(delim))
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
return s
|
|
176
|
+
}, s)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Translates an AdaptError
|
|
181
|
+
* @param {String} lang The target language
|
|
182
|
+
* @param {AdaptError} error Error to translate
|
|
183
|
+
* @returns The translated error (if passed error is not an instance of AdaptError, the original value will be returned)
|
|
184
|
+
*/
|
|
185
|
+
translateError (lang, error) {
|
|
186
|
+
return error?.constructor?.name === 'AdaptError' ? this.translate(lang, `error.${error.code}`, error.data) : error
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default LangModule
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adapt-authoring-lang",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Localisation for the Adapt authoring tool",
|
|
5
|
+
"homepage": "https://github.com/taylortom/adapt-authoring-lang",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"at-langcheck": "./bin/check.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": "github:adapt-security/adapt-authoring-lang",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"glob": "^11.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"eslint": "^9.12.0",
|
|
18
|
+
"standard": "^17.1.0"
|
|
19
|
+
}
|
|
20
|
+
}
|