Skip to content

Commit 0546e16

Browse files
authored
feat: add generics to Storage (#1649)
1 parent 2dbae21 commit 0546e16

File tree

8 files changed

+64
-54
lines changed

8 files changed

+64
-54
lines changed

package-lock.json

Lines changed: 2 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"chalk": "^5.4.1",
5858
"debug": "^4.4.1",
5959
"execa": "^9.5.3",
60-
"json-schema": "^0.4.0",
6160
"latest-version": "^9.0.0",
6261
"lodash-es": "^4.17.21",
6362
"mem-fs-editor": "^11.1.4",
@@ -66,12 +65,12 @@
6665
"semver": "^7.7.2",
6766
"simple-git": "^3.27.0",
6867
"sort-keys": "^5.1.0",
69-
"text-table": "^0.2.0"
68+
"text-table": "^0.2.0",
69+
"type-fest": "^4.41.0"
7070
},
7171
"devDependencies": {
7272
"@types/debug": "^4.1.12",
7373
"@types/ejs": "^3.1.5",
74-
"@types/json-schema": "^7.0.15",
7574
"@types/minimist": "^1.2.5",
7675
"@types/semver": "^7.7.0",
7776
"@types/sinon": "^17.0.4",
@@ -88,7 +87,6 @@
8887
"prettier": "3.5.3",
8988
"prettier-plugin-packagejson": "2.5.12",
9089
"sinon": "^20.0.0",
91-
"type-fest": "^4.41.0",
9290
"typescript": "5.8.3",
9391
"vitest": "^3.1.3",
9492
"yeoman-assert": "^3.1.1",

src/actions/fs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export class FsMixin {
348348
*/
349349
_templateData<D extends TemplateData = TemplateData>(this: BaseGenerator, path?: string): D {
350350
if (path) {
351-
return this.config.getPath(path);
351+
return this.config.getPath(path) as any as D;
352352
}
353353

354354
const allConfig: D = this.config.getAll() as D;

src/generator.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { PackageJsonMixin } from './actions/package-json.js';
2323
import { SpawnCommandMixin } from './actions/spawn-command.js';
2424
import { GitMixin } from './actions/user.js';
2525
import { TasksMixin } from './actions/lifecycle.js';
26+
import type { PackageJson } from 'type-fest';
2627

2728
type Environment = BaseEnvironment<QueuedAdapter>;
2829

@@ -33,10 +34,17 @@ const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@';
3334

3435
const ENV_VER_WITH_VER_API = '2.9.0';
3536

36-
const packageJson = JSON.parse(readFileSync(pathJoin(_dirname, '../package.json'), 'utf8'));
37+
const packageJson: PackageJson = JSON.parse(readFileSync(pathJoin(_dirname, '../package.json'), 'utf8'));
3738

3839
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
39-
export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFeatures = BaseFeatures>
40+
export class BaseGenerator<
41+
ConfigType extends Record<any, any> = Record<any, any>,
42+
O extends BaseOptions = BaseOptions,
43+
F extends BaseFeatures = BaseFeatures,
44+
GeneratorConfigType extends Record<any, any> = Record<any, any>,
45+
InstanceConfigType extends Record<any, any> = Record<any, any>,
46+
GlobalConfigType extends Record<any, any> = Record<any, any>,
47+
>
4048
extends EventEmitter
4149
implements Omit<GeneratorApi<O, F>, 'features'>
4250
{
@@ -68,12 +76,12 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
6876
_contextMap?: Map<string, any>;
6977
_sourceRoot!: string;
7078

71-
generatorConfig?: Storage;
72-
instanceConfig?: Storage;
73-
_config?: Storage;
74-
_packageJson?: Storage;
79+
generatorConfig?: Storage<GeneratorConfigType>;
80+
instanceConfig?: Storage<InstanceConfigType>;
81+
_config?: Storage<ConfigType>;
82+
_packageJson?: Storage<PackageJson>;
7583

76-
_globalConfig!: Storage;
84+
_globalConfig!: Storage<GlobalConfigType>;
7785

7886
// If for some reason environment adds more queues, we should use or own for stability.
7987
static get queues() {
@@ -92,7 +100,7 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
92100

93101
_running = false;
94102
readonly features!: F;
95-
readonly yoGeneratorVersion: string = packageJson.version as string;
103+
readonly yoGeneratorVersion: string = packageJson.version!;
96104

97105
/**
98106
* @classdesc The `Generator` class provides the common API shared by all generators.
@@ -630,9 +638,9 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
630638
/**
631639
* Generator config Storage.
632640
*/
633-
get config() {
641+
get config(): Storage<ConfigType> {
634642
if (!this._config) {
635-
this._config = this._getStorage();
643+
this._config = this._getStorage<ConfigType>();
636644
}
637645

638646
return this._config;
@@ -657,7 +665,7 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
657665
* },
658666
* });
659667
*/
660-
get packageJson(): Storage {
668+
get packageJson(): Storage<PackageJson> {
661669
if (!this._packageJson) {
662670
this._packageJson = this.createStorage('package.json');
663671
}
@@ -699,7 +707,11 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
699707
* @param storePath The path of the json file
700708
* @param options storage options or the storage name
701709
*/
702-
createStorage(storePath: string, options?: string | StorageOptions): Storage {
710+
createStorage<StoredType extends Record<any, any> = Record<any, any>>(
711+
storePath: string,
712+
options?: string | StorageOptions,
713+
): Storage<StoredType>;
714+
createStorage(storePath: string, options?: string | StorageOptions): Storage<any> {
703715
if (typeof options === 'string') {
704716
options = { name: options };
705717
}
@@ -714,13 +726,16 @@ export class BaseGenerator<O extends BaseOptions = BaseOptions, F extends BaseFe
714726
* @param options Storage options
715727
* @return Generator storage
716728
*/
717-
_getStorage(rootName: string | StorageOptions = this.rootGeneratorName(), options: StorageOptions = {}) {
729+
_getStorage<StoredType extends Record<any, any> = Record<any, any>>(
730+
rootName: string | StorageOptions = this.rootGeneratorName(),
731+
options: StorageOptions = {},
732+
): Storage<StoredType> {
718733
if (typeof rootName === 'object') {
719734
options = rootName;
720735
rootName = this.rootGeneratorName();
721736
}
722737

723-
return this.createStorage('.yo-rc.json', { ...options, name: rootName });
738+
return this.createStorage<StoredType>('.yo-rc.json', { ...options, name: rootName });
724739
}
725740

726741
/**

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ export type * from './util/storage.js';
1010
export { default as Storage } from './util/storage.js';
1111

1212
export default class Generator<
13+
C extends Record<any, any> = Record<any, any>,
1314
O extends BaseOptions = BaseOptions,
1415
F extends BaseFeatures = BaseFeatures,
15-
> extends BaseGenerator<O, F> {
16+
> extends BaseGenerator<C, O, F> {
1617
_simpleGit?: SimpleGit;
1718

1819
constructor(...args: any[]) {

src/types.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import type {
55
GeneratorOptions as OptionsApi,
66
ProgressOptions,
77
} from '@yeoman/types';
8-
import type { JSONSchema7Type } from 'json-schema';
98
import type { PipelineOptions } from 'mem-fs';
109
import type { MemFsEditorFile } from 'mem-fs-editor';
10+
import type { JsonValue } from 'type-fest';
1111
import type Storage from './util/storage.js';
1212
import type Generator from './index.js';
1313

14-
export type StorageValue = JSONSchema7Type;
15-
export type StorageRecord = Record<string, StorageValue>;
14+
export type StorageValue = JsonValue;
1615
export type GeneratorPipelineOptions = PipelineOptions<MemFsEditorFile> & ProgressOptions & { pendingFiles?: boolean };
1716

1817
/**

src/util/prompt-suggestion.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import assert from 'node:assert';
2-
import type { JSONSchema7Object } from 'json-schema';
32
import type { PromptAnswers, PromptQuestion } from '../questions.js';
43
import type Storage from './storage.js';
54

@@ -173,7 +172,7 @@ export const storeAnswers = (store: Storage, questions: any, answers: PromptAnsw
173172
assert.ok(typeof answers === 'object', 'answers must be a object');
174173

175174
storeAll = storeAll || false;
176-
const promptValues = store.get<JSONSchema7Object>('promptValues') ?? {};
175+
const promptValues = store.get('promptValues') ?? {};
177176

178177
questions = [questions].flat();
179178

src/util/storage.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import assert from 'node:assert';
22
import { cloneDeep, get, merge, set, defaults as setDefaults } from 'lodash-es';
33
import sortKeys from 'sort-keys';
4+
import type { Get } from 'type-fest';
45
import type { MemFsEditor } from 'mem-fs-editor';
5-
import type { StorageRecord, StorageValue } from '../types.js';
6+
import type { StorageValue } from '../types.js';
67

78
/**
89
* Proxy handler for Storage
@@ -71,7 +72,7 @@ export type StorageOptions = {
7172
* }
7273
* }
7374
*/
74-
class Storage {
75+
class Storage<StorageRecord extends Record<string, any> = Record<string, any>> {
7576
path: string;
7677
name?: string;
7778
fs: MemFsEditor;
@@ -196,7 +197,7 @@ class Storage {
196197
if (this.lodashPath) {
197198
set(fullStore, this.name, value);
198199
} else {
199-
fullStore[this.name] = value;
200+
(fullStore as any)[this.name] = value;
200201
}
201202
} else {
202203
fullStore = value;
@@ -217,17 +218,17 @@ class Storage {
217218
* @param key The key under which the value is stored.
218219
* @return The stored value. Any JSON valid type could be returned
219220
*/
220-
get<T extends StorageValue = StorageValue>(key: string): T {
221-
return this._store[key] as T;
221+
get<const Key extends keyof StorageRecord>(key: Key): StorageRecord[Key] {
222+
return this._store[key];
222223
}
223224

224225
/**
225226
* Get a stored value from a lodash path
226227
* @param path The path under which the value is stored.
227228
* @return The stored value. Any JSON valid type could be returned
228229
*/
229-
getPath<T extends StorageValue = StorageValue>(path: string): T {
230-
return get(this._store, path) as T;
230+
getPath<const KeyPath extends string>(path: KeyPath): Get<StorageValue, KeyPath> {
231+
return get(this._store, path);
231232
}
232233

233234
/**
@@ -244,21 +245,26 @@ class Storage {
244245
* @param val Any valid JSON type value (String, Number, Array, Object).
245246
* @return val Whatever was passed in as val.
246247
*/
247-
set<V = StorageValue>(value: V): V;
248-
set<V = StorageValue>(key: string | number, value?: V): V | undefined;
249-
set<V = StorageValue>(key: string | number | V, value?: V): V | undefined {
248+
set(value: Partial<StorageRecord>): StorageRecord;
249+
set<const Key extends keyof StorageRecord, const Value extends StorageRecord[Key]>(
250+
key: Key,
251+
value?: Value,
252+
): Value | undefined;
253+
set(key: string | number | Partial<StorageRecord>, value?: StorageValue): StorageRecord | StorageValue | undefined {
250254
const store = this._store;
255+
let ret: StorageValue | StorageValue | undefined;
251256

252257
if (typeof key === 'object') {
253-
value = Object.assign(store, key);
258+
ret = Object.assign(store, key);
254259
} else if (typeof key === 'string' || typeof key === 'number') {
255-
store[key] = value as any;
260+
(store as any)[key] = value;
261+
ret = value;
256262
} else {
257263
throw new TypeError(`key not supported ${typeof key}`);
258264
}
259265

260266
this._persist(store);
261-
return value;
267+
return ret;
262268
}
263269

264270
/**
@@ -267,7 +273,7 @@ class Storage {
267273
* @param val Any valid JSON type value (String, Number, Array, Object).
268274
* @return val Whatever was passed in as val.
269275
*/
270-
setPath(path: string | number, value: StorageValue) {
276+
setPath<const KeyPath extends string>(path: KeyPath, value: Get<StorageValue, KeyPath>): Get<StorageValue, KeyPath> {
271277
assert(typeof value !== 'function', "Storage value can't be a function");
272278

273279
const store = this._store;
@@ -280,7 +286,7 @@ class Storage {
280286
* Delete a key from the store and schedule a save.
281287
* @param key The key under which the value is stored.
282288
*/
283-
delete(key: string) {
289+
delete(key: keyof StorageRecord): void {
284290
const store = this._store;
285291

286292
delete store[key];
@@ -293,7 +299,7 @@ class Storage {
293299
* @param defaults Key-value object to store.
294300
* @return val Returns the merged options.
295301
*/
296-
defaults(defaults: StorageRecord): StorageRecord {
302+
defaults(defaults: Partial<StorageRecord>): StorageRecord {
297303
assert(typeof defaults === 'object', 'Storage `defaults` method only accept objects');
298304
const store = setDefaults({}, this._store, defaults);
299305
this._persist(store);
@@ -304,7 +310,7 @@ class Storage {
304310
* @param defaults Key-value object to store.
305311
* @return val Returns the merged object.
306312
*/
307-
merge(source: StorageRecord) {
313+
merge(source: Partial<StorageRecord>): StorageRecord {
308314
assert(typeof source === 'object', 'Storage `merge` method only accept objects');
309315
const value = merge({}, this._store, source);
310316
this._persist(value);
@@ -317,9 +323,9 @@ class Storage {
317323
* Some paths need to be escaped. Eg: ["dotted.path"]
318324
* @return Returns a new Storage.
319325
*/
320-
createStorage(path: string): Storage {
326+
createStorage<const KeyPath extends string>(path: KeyPath): Storage<Get<StorageRecord, KeyPath>> {
321327
const childName = this.name ? `${this.name}.${path}` : path;
322-
return new Storage(childName, this.fs, this.path, { lodashPath: true });
328+
return new Storage<Get<StorageRecord, KeyPath>>(childName, this.fs, this.path, { lodashPath: true });
323329
}
324330

325331
/**

0 commit comments

Comments
 (0)