Skip to content

Commit 0f8ac5c

Browse files
committed
feat: build license file for vscode-webdriverio
1 parent 6cb13c9 commit 0f8ac5c

6 files changed

Lines changed: 1127 additions & 0 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
},
2020
"[typescript]": {
2121
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
22+
},
23+
"[markdown]": {
24+
"files.trimTrailingWhitespace": false
2225
}
2326
}

infra/compiler/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"build": "tsx ./src/index.ts"
77
},
88
"dependencies": {
9+
"chalk": "^5.4.1",
910
"esbuild": "^0.25.0",
11+
"fdir": "^6.4.6",
1012
"type-fest": "^4.24.0"
1113
}
1214
}

infra/compiler/src/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import path from 'node:path'
33
import url from 'node:url'
44
import { parseArgs } from 'node:util'
55

6+
import chalk from 'chalk'
67
import { context } from 'esbuild'
78

9+
import { checkLicense } from './license.js'
810
import { esbuildProblemMatcherPlugin } from './plugins.js'
911

1012
import type { PackageJson } from 'type-fest'
@@ -89,5 +91,21 @@ if (options.watch) {
8991

9092
if (!options.production) {
9193
fss.writeFileSync(path.join(outdir, 'meta.json'), JSON.stringify(result.metafile))
94+
95+
const baseLicense = path.join(rootDir, 'LICENSE')
96+
const checker = checkLicense(pkgPath, result.metafile)
97+
const license = checker.render(baseLicense)
98+
99+
const existingLicenseText = fss.existsSync(license.file)
100+
? fss.readFileSync(license.file, { encoding: 'utf-8' })
101+
: ''
102+
if (existingLicenseText !== license.contents) {
103+
fss.writeFileSync(license.file, license.contents, { encoding: 'utf-8' })
104+
console.info(
105+
chalk.yellow(
106+
`\n${path.relative(rootDir, license.file)} was updated. You should commit the updated file.\n`
107+
)
108+
)
109+
}
92110
}
93111
}

infra/compiler/src/license.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import fss from 'node:fs'
2+
import path from 'node:path'
3+
4+
import { fdir } from 'fdir'
5+
6+
type LicenseData = {
7+
name: string
8+
version: string
9+
author?: string | { name: string }
10+
repository?: { url: string }
11+
license: string
12+
licenseText: string
13+
noticeText: string | null
14+
}
15+
16+
export function checkLicense(pkgPath: string, meta: any) {
17+
const inputs = Object.keys(meta.inputs)
18+
const checker = new LicenseChecker(path.dirname(pkgPath))
19+
20+
for (const input of inputs) {
21+
if (input.match(/node_modules/)) {
22+
const match = Array.from(input.matchAll(/node_modules\/((@[^/]+\/)?[^/]+)/g))
23+
24+
const relativePath = input.substring(0, match[match.length - 1].index)
25+
const absEntryPoint = path.resolve(path.dirname(pkgPath), input)
26+
const absPackageRoot = path.resolve(path.dirname(pkgPath), relativePath)
27+
const maxDepth = absEntryPoint.split(path.posix.sep).length - absPackageRoot.split(path.posix.sep).length
28+
checker.findPackageJson(absEntryPoint, maxDepth)
29+
}
30+
}
31+
return checker
32+
}
33+
34+
class LicenseChecker {
35+
private _cache = new Map<string, LicenseData>()
36+
public licenseTypeDependencies = new Set<string>()
37+
public dependencies = new Map<string, LicenseData>()
38+
39+
constructor(private _rootDir: string) {}
40+
41+
findPackageJson(entryPoint: string, maxDepth: number) {
42+
let dir = path.dirname(entryPoint)
43+
let pkg = null
44+
let cntUpDir = 0
45+
46+
while (cntUpDir < maxDepth) {
47+
if (this._cache.has(dir)) {
48+
pkg = this._cache.get(dir)
49+
break
50+
}
51+
const pkgPath = path.join(dir, 'package.json')
52+
const exists = fss.existsSync(pkgPath)
53+
if (exists) {
54+
const pkgJson = JSON.parse(fss.readFileSync(pkgPath, { encoding: 'utf-8' }))
55+
const license = pkgJson.license || pkgJson.licenses
56+
const { name, version } = pkgJson
57+
const hasLicense = license && license.length > 0
58+
if ((name && version) || hasLicense) {
59+
// found
60+
const licenseText = readFile(dir, ['license', 'licence'])
61+
if (!licenseText) {
62+
throw new Error(`License text is not found: ${entryPoint}`)
63+
}
64+
65+
const noticeText = readFile(dir, ['notice'])
66+
pkg = pkgJson as LicenseData
67+
pkg.licenseText = licenseText
68+
pkg.noticeText = noticeText
69+
this.licenseTypeDependencies.add(license)
70+
this._cache.set(dir, pkg)
71+
break
72+
}
73+
}
74+
cntUpDir++
75+
dir = path.resolve(path.join(dir, '..'))
76+
}
77+
if (pkg) {
78+
this.dependencies.set(this._generateKey(pkg.name, pkg.version), pkg)
79+
} else {
80+
throw new Error(`License is not found: ${entryPoint}`)
81+
}
82+
return pkg
83+
}
84+
85+
private _generateKey(name: string, version: string) {
86+
return `${name}@${version}`
87+
}
88+
89+
private _getPkgName() {
90+
const pkgJson = path.join(this._rootDir, 'package.json')
91+
const pkg = JSON.parse(fss.readFileSync(pkgJson, { encoding: 'utf-8' })) as { name: string }
92+
return pkg.name
93+
}
94+
95+
render(baseLicense: string) {
96+
const baseLicenseText = fss.readFileSync(baseLicense, { encoding: 'utf-8' })
97+
98+
const contents: string[] = []
99+
contents.push(`# License of ${this._getPkgName()}`)
100+
contents.push(`${this._getPkgName()} is released under the MIT license: `)
101+
contents.push('')
102+
contents.push(
103+
baseLicenseText
104+
.replace(/\n\r|\r/g, '\n')
105+
.split('\n')
106+
.map((line) => `${line} `)
107+
.join('\n')
108+
)
109+
contents.push('# Licenses of bundled dependencies')
110+
contents.push('The published extension contains additionally code with the following licenses: ')
111+
contents.push(Array.from(this.licenseTypeDependencies).sort().join(', '))
112+
contents.push('')
113+
contents.push('# Bundled dependencies:')
114+
const dependencies: string[] = []
115+
const sortedDependencies = new Map([...this.dependencies].sort())
116+
sortedDependencies.forEach((value) => {
117+
const lines: string[] = []
118+
lines.push(`## ${value.name} `)
119+
lines.push(`License: ${value.license} `)
120+
if (value.author) {
121+
if (typeof value.author === 'object') {
122+
lines.push(`Author: ${value.author.name} `)
123+
} else {
124+
lines.push(`Author: ${value.author.split('<')[0].split('(')[0].trim()} `)
125+
}
126+
}
127+
if (value.repository && value.repository.url) {
128+
lines.push(`Repository: ${value.repository.url} `)
129+
}
130+
lines.push('### License Text')
131+
lines.push(
132+
value.licenseText
133+
.replace(/\n\r|\r/g, '\n')
134+
.split('\n')
135+
.map((line) => `> ${line}`)
136+
.join('\n')
137+
)
138+
139+
if (value.noticeText) {
140+
lines.push('### Notice Text')
141+
lines.push(
142+
value.noticeText
143+
.replace(/\n\r|\r/g, '\n')
144+
.split('\n')
145+
.map((line) => `> ${line}`)
146+
.join('\n')
147+
)
148+
}
149+
dependencies.push(lines.join('\n'))
150+
})
151+
contents.push(dependencies.join('\n\n---------------------------------------\n\n'))
152+
153+
const licenseFile = path.join(this._rootDir, 'LICENSE.md')
154+
return {
155+
file: licenseFile,
156+
contents: contents.join('\n'),
157+
}
158+
}
159+
}
160+
161+
function readFile(dir: string, inputs: string[]) {
162+
for (const input of inputs) {
163+
const absolutePath = path.join(dir, input)
164+
const relativeToDir = path.relative(dir, absolutePath)
165+
const findings = new fdir().withRelativePaths().filter(pathsMatch(relativeToDir)).crawl(dir).sync()
166+
const firstPath = findings[0]
167+
if (firstPath) {
168+
const file = path.join(dir, firstPath)
169+
return fss.readFileSync(file, 'utf-8')
170+
}
171+
}
172+
return null
173+
}
174+
175+
/**
176+
* Returns a predicate function that returns `true` if the given path matches the target path.
177+
*
178+
* @param {string} target Target path.
179+
* @returns {function(*): boolean} Predicate function.
180+
*/
181+
function pathsMatch(target: string): (path: any) => boolean {
182+
const targetRegExp = generatePattern(target)
183+
return (p) => targetRegExp.test(p)
184+
}
185+
186+
/**
187+
* Generate a pattern where all regexp special characters are escaped.
188+
* @param {string} input Input.
189+
* @returns {string} Escaped input.
190+
*/
191+
function escapeRegExp(input: string): string {
192+
return input.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
193+
}
194+
195+
/**
196+
* Generate filename pattern for the given input: the generated regexp will match any file
197+
* starting with `input` (case insensitively).
198+
*
199+
* @param {string} input Input.
200+
* @returns {RegExp} Generated pattern.
201+
*/
202+
function generatePattern(input: string): RegExp {
203+
const FILE_FORBIDDEN_CHARACTERS = ['#', '%', '&', '*', ':', '<', '>', '?', '/', path.sep, '{', '|', '}'].map((c) =>
204+
escapeRegExp(c)
205+
)
206+
207+
const FILE_SUFFIX_PTN = `[^${FILE_FORBIDDEN_CHARACTERS.join('')}]`
208+
return new RegExp(`^${input}(${FILE_SUFFIX_PTN})*$`, 'i')
209+
}

0 commit comments

Comments
 (0)