Skip to content

Commit 8fffed4

Browse files
committed
feat(config): addon queries feature
now you can use queries instead of import and instantiate classes
1 parent ee50c24 commit 8fffed4

File tree

5 files changed

+250
-6
lines changed

5 files changed

+250
-6
lines changed

packages/config/.eslintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"project": ["./tsconfig.json"]
1010
},
1111
"rules": {
12-
"@typescript-eslint/naming-convention": "off"
12+
"@typescript-eslint/naming-convention": "off",
13+
"@typescript-eslint/no-explicit-any": "off",
14+
"@typescript-eslint/no-unsafe-call": "off",
15+
"@typescript-eslint/no-unsafe-return": "off"
1316
}
1417
}

packages/config/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,44 @@ await load() // Returns config object or null
5757
await load({ config: true }) // Returns config object or throws an error if config is not found
5858
await load({ [propertyName]: true }) // Returns config object with desired property or null or throws an error if property is not set in config
5959
```
60+
61+
Also loader has a feature to load and instantiate addon's classes. For example, if you have a config like this:
62+
63+
```js
64+
export const project = [
65+
'@simple-release/pnpm#PnpmWorkspacesProject',
66+
{
67+
mode: 'independent'
68+
}
69+
]
70+
```
71+
72+
You can load it like this:
73+
74+
```js
75+
import { load } from '@simple-release/config'
76+
77+
const config = await load()
78+
79+
config.project // Will be an instance of PnpmWorkspacesProject
80+
```
81+
82+
You can pass your own loader and use queries with version:
83+
84+
```js
85+
export const project = [
86+
'@simple-release/[email protected]#PnpmWorkspacesProject',
87+
{
88+
mode: 'independent'
89+
}
90+
]
91+
```
92+
93+
```js
94+
import { load } from '@simple-release/config'
95+
96+
const config = await load({}, async (name, version) => {
97+
await install(name, version) // For example you can implement lazy install here
98+
return import(name)
99+
})
100+
```

packages/config/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@
4747
"postpublish": "pnpm clear:package",
4848
"build": "tsc -p tsconfig.build.json",
4949
"lint": "eslint --parser-options tsconfigRootDir:. '**/*.{js,ts}'",
50+
"test:unit": "vitest run --coverage",
5051
"test:types": "tsc --noEmit",
51-
"test": "run -p lint test:types"
52+
"test": "run -p lint test:unit test:types"
5253
},
5354
"dependencies": {
5455
"@simple-release/core": "workspace:^",

packages/config/src/index.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { join } from 'path'
2+
import {
3+
vi,
4+
describe,
5+
it,
6+
expect
7+
} from 'vitest'
8+
import { PackageJsonProject } from '@simple-release/core'
9+
import {
10+
parseImportQuery,
11+
loadClass
12+
} from './index.js'
13+
14+
describe('config', () => {
15+
describe('parseImportQuery', () => {
16+
it('should parse a simple import query', () => {
17+
expect(
18+
parseImportQuery('path/to/module')
19+
).toEqual({
20+
path: 'path/to/module'
21+
})
22+
})
23+
24+
it('should parse an import query with version', () => {
25+
expect(
26+
parseImportQuery('path/to/[email protected]')
27+
).toEqual({
28+
path: 'path/to/module',
29+
version: '1.0.0'
30+
})
31+
})
32+
33+
it('should parse an import query with symbol', () => {
34+
expect(
35+
parseImportQuery('path/to/module#MyClass')
36+
).toEqual({
37+
path: 'path/to/module',
38+
symbol: 'MyClass'
39+
})
40+
})
41+
42+
it('should parse an import query with version and symbol', () => {
43+
expect(
44+
parseImportQuery('path/to/[email protected]#MyClass')
45+
).toEqual({
46+
path: 'path/to/module',
47+
version: '1.0.0',
48+
symbol: 'MyClass'
49+
})
50+
})
51+
52+
it('should parse an import query with scoped package', () => {
53+
expect(
54+
parseImportQuery('@simple-release/core#PackageJsonProject')
55+
).toEqual({
56+
path: '@simple-release/core',
57+
symbol: 'PackageJsonProject'
58+
})
59+
})
60+
})
61+
62+
describe('loadClass', () => {
63+
it('should load a class from a module', async () => {
64+
const project = await loadClass(
65+
[
66+
'@simple-release/core#PackageJsonProject',
67+
{
68+
path: join(__dirname, '..', 'package.json')
69+
}
70+
],
71+
{}
72+
)
73+
74+
expect(project).toBeInstanceOf(PackageJsonProject)
75+
})
76+
77+
it('should use custom loader', async () => {
78+
const loader = vi.fn(() => Promise.resolve({
79+
PackageJsonProject
80+
}))
81+
const project = await loadClass(
82+
[
83+
'@simple-release/[email protected]#PackageJsonProject',
84+
{
85+
path: join(__dirname, '..', 'package.json')
86+
}
87+
],
88+
{
89+
config: 'yes'
90+
} as any,
91+
loader
92+
)
93+
94+
expect(project).toBeInstanceOf(PackageJsonProject)
95+
expect(loader).toHaveBeenCalledWith('@simple-release/core', '1.0.0', {
96+
config: 'yes'
97+
})
98+
})
99+
})
100+
})

packages/config/src/index.ts

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {
1010
const VARIANTS = [
1111
'.simple-release.js',
1212
'.simple-release.mjs',
13-
'.simple-release.cjs'
13+
'.simple-release.cjs',
14+
'.simple-release.json'
1415
]
1516

1617
export interface SimpleReleaseConfig<
@@ -46,6 +47,18 @@ type Result<
4647
? ApplyConfigRequirement<R, C>
4748
: never
4849

50+
interface ParsedImportQuery {
51+
path: string
52+
version?: string
53+
symbol?: string
54+
}
55+
56+
type Loader<T = Record<string, any>> = (
57+
path: string,
58+
version: string | undefined,
59+
rawConfig: Record<string, any>
60+
) => Promise<T>
61+
4962
function validate(target: Record<string, any>, rules: Record<string, any>) {
5063
for (const [rule, required] of Object.entries(rules)) {
5164
if (required && target[rule] === undefined) {
@@ -54,18 +67,101 @@ function validate(target: Record<string, any>, rules: Record<string, any>) {
5467
}
5568
}
5669

70+
const IMPORT_QUERY_REGEX = /^(@?[^@#]*)(@[^#]+)?(#.*)?$/
71+
72+
export function parseImportQuery(
73+
importPath: string
74+
): ParsedImportQuery {
75+
const match = importPath.match(IMPORT_QUERY_REGEX)
76+
77+
if (!match) {
78+
return {
79+
path: importPath
80+
}
81+
}
82+
83+
const [
84+
,
85+
path,
86+
version,
87+
symbol
88+
] = match
89+
90+
return {
91+
path,
92+
version: version?.slice(1),
93+
symbol: symbol?.slice(1)
94+
}
95+
}
96+
97+
export async function loadClass<T, C extends SimpleReleaseConfig>(
98+
queryWithOptions: string | [string, Record<string, any>],
99+
config: C,
100+
loader: Loader = _ => import(_)
101+
) {
102+
const [query, options] = Array.isArray(queryWithOptions)
103+
? queryWithOptions
104+
: [queryWithOptions]
105+
const {
106+
path,
107+
version,
108+
symbol
109+
} = parseImportQuery(query)
110+
const module = await loader(path, version, config)
111+
const inst = new module[symbol || 'default'](options) as T
112+
113+
return inst
114+
}
115+
116+
export function isQuery(value: unknown): value is string | [string, Record<string, any>] {
117+
return typeof value === 'string' || (
118+
Array.isArray(value)
119+
&& value.length === 2
120+
&& typeof value[0] === 'string'
121+
&& value[1] && typeof value[1] === 'object'
122+
)
123+
}
124+
125+
export function getQuery(queryWithOptions: unknown): string | null {
126+
return isQuery(queryWithOptions)
127+
? typeof queryWithOptions === 'string'
128+
? queryWithOptions
129+
: queryWithOptions[0]
130+
: null
131+
}
132+
133+
async function loadAndSetIfQuery(
134+
config: SimpleReleaseConfig,
135+
key: keyof SimpleReleaseConfig,
136+
loader?: Loader
137+
) {
138+
const value = config[key]
139+
140+
if (value && isQuery(value)) {
141+
// eslint-disable-next-line require-atomic-updates
142+
config[key] = await loadClass(value, config, loader)
143+
}
144+
}
145+
57146
/**
58147
* Load simple-release config.
59148
* @param requirements
149+
* @param loader
60150
* @returns simple-release config
61151
*/
62152
export async function load<
63153
P extends Project = Project,
64154
G extends GitRepositoryHosting = GitRepositoryHosting,
65155
R extends SimpleReleaseConfigRequirements = SimpleReleaseConfigRequirements
66-
>(requirements?: R): Promise<Result<P, G, R>>
67-
68-
export async function load(requirements: Record<string, any> = {}) {
156+
>(
157+
requirements?: R,
158+
loader?: Loader
159+
): Promise<Result<P, G, R>>
160+
161+
export async function load(
162+
requirements: Record<string, any> = {},
163+
loader?: Loader
164+
) {
69165
const {
70166
config: configRequired,
71167
...reqs
@@ -81,6 +177,9 @@ export async function load(requirements: Record<string, any> = {}) {
81177

82178
validate(config, reqs)
83179

180+
await loadAndSetIfQuery(config, 'project', loader)
181+
await loadAndSetIfQuery(config, 'hosting', loader)
182+
84183
return config
85184
} catch (err) {
86185
if (configRequired) {

0 commit comments

Comments
 (0)