From 653ae3e60afdac12bd87feb50a7d627f7e0335bc Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 4 Aug 2024 01:33:03 +0900 Subject: [PATCH] feat: enhance the diversification of configuration file reading - enhance configuration reading so that it can read from .ctirc, package.json, and tsconfig.json. - enhance visibility of the source of configuration from .ctirc, package.json, or tsconfig.json. --- package.json | 2 + pnpm-lock.yaml | 11 + .../__tests__/get.config.value.test.ts | 18 ++ src/configs/castConfig.ts | 9 +- src/configs/getConfigObject.ts | 17 ++ src/configs/loadConfig.ts | 165 +++++++++------ .../modules/__tests__/config.file.test.ts | 194 ++++++++++++++++++ src/configs/modules/getConfigFilePath.ts | 16 ++ src/configs/modules/readConfigFromFile.ts | 22 ++ .../modules/readConfigFromPackageJson.ts | 30 +++ .../modules/readConfigFromTsconfigJson.ts | 34 +++ src/configs/transforms/createBuildOptions.ts | 8 +- src/modules/commands/bundling.ts | 11 +- .../path/__tests__/config.path.module.test.ts | 101 --------- src/modules/path/getConfigFilePath.ts | 30 --- .../values/__tests__/valid.type.test.ts | 20 ++ src/modules/values/getCheckedValue.ts | 11 + 17 files changed, 504 insertions(+), 195 deletions(-) create mode 100644 src/configs/getConfigObject.ts create mode 100644 src/configs/modules/__tests__/config.file.test.ts create mode 100644 src/configs/modules/getConfigFilePath.ts create mode 100644 src/configs/modules/readConfigFromFile.ts create mode 100644 src/configs/modules/readConfigFromPackageJson.ts create mode 100644 src/configs/modules/readConfigFromTsconfigJson.ts delete mode 100644 src/modules/path/getConfigFilePath.ts create mode 100644 src/modules/values/__tests__/valid.type.test.ts create mode 100644 src/modules/values/getCheckedValue.ts diff --git a/package.json b/package.json index 093e096..59f5d33 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@types/minimist": "^1.2.4", "@types/node": "^20.8.7", "@types/source-map-support": "^0.5.9", + "@types/type-check": "^0.3.30", "@types/yargs": "^17.0.32", "@typescript-eslint/eslint-plugin": "^7.6.0", "@typescript-eslint/parser": "^7.6.0", @@ -143,6 +144,7 @@ "ts-morph": "^23.0.0", "ts-pattern": "^5.0.5", "tslib": "^2.6.2", + "type-check": "^0.4.0", "type-fest": "^4.15.0", "yaml": "^2.4.1", "yargs": "^17.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2964bbb..8a17faa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: tslib: specifier: ^2.6.2 version: 2.6.2 + type-check: + specifier: ^0.4.0 + version: 0.4.0 type-fest: specifier: ^4.15.0 version: 4.15.0 @@ -135,6 +138,9 @@ importers: '@types/source-map-support': specifier: ^0.5.9 version: 0.5.10 + '@types/type-check': + specifier: ^0.3.30 + version: 0.3.30 '@types/yargs': specifier: ^17.0.32 version: 17.0.32 @@ -871,6 +877,9 @@ packages: '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + '@types/type-check@0.3.30': + resolution: {integrity: sha512-xTxWk/TWmpYBWD47NKejJiJ0y14MgPx8XOF2kDSqhQGWPy2qmuKc7P854Wrn3SqO/LyEYiVdsGC1mJw1ds+QpQ==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -3751,6 +3760,8 @@ snapshots: dependencies: '@types/node': 20.9.0 + '@types/type-check@0.3.30': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.32': diff --git a/src/configs/__tests__/get.config.value.test.ts b/src/configs/__tests__/get.config.value.test.ts index bb28950..0f48fd0 100644 --- a/src/configs/__tests__/get.config.value.test.ts +++ b/src/configs/__tests__/get.config.value.test.ts @@ -1,3 +1,4 @@ +import { getConfigObject } from '#/configs/getConfigObject'; import { getConfigValue } from '#/configs/getConfigValue'; import { describe, expect, it } from 'vitest'; @@ -28,3 +29,20 @@ describe('getConfigValue', () => { expect(r02).toBeUndefined(); }); }); + +describe('getConfigObject', () => { + it('key more then one', () => { + const obj = getConfigObject({ a: 'a', b: 'b', c: 'c' }, 'a', 'c'); + expect(obj).toMatchObject({ a: 'a', c: 'c' }); + }); + + it('empty keys', () => { + const obj = getConfigObject({ a: 'a', b: 'b', c: 'c' }, 'z'); + expect(obj).toBeUndefined(); + }); + + it('hit two keys, but undefined value', () => { + const obj = getConfigObject({ a: 'a', b: 'b', c: 'c', d: undefined, e: undefined }, 'd', 'e'); + expect(obj).toBeUndefined(); + }); +}); diff --git a/src/configs/castConfig.ts b/src/configs/castConfig.ts index e354715..ca98d72 100644 --- a/src/configs/castConfig.ts +++ b/src/configs/castConfig.ts @@ -6,12 +6,17 @@ import type { TCommandRemoveOptions } from '#/configs/interfaces/TCommandRemoveO export function castConfig( command: CE_CTIX_COMMAND, config: unknown, - paths: { config?: string; tsconfig?: string }, + paths: { + from: 'none' | '.ctirc' | 'tsconfig.json' | 'package.json'; + config?: string; + tsconfig?: string; + }, ): TCommandBuildArgvOptions | TCommandRemoveOptions | IProjectOptions { switch (command) { case CE_CTIX_COMMAND.BUILD_COMMAND: return { ...(config as TCommandBuildArgvOptions), + from: paths.from, p: paths.tsconfig, project: paths.tsconfig, c: paths.config, @@ -21,6 +26,7 @@ export function castConfig( case CE_CTIX_COMMAND.REMOVE_COMMAND: return { ...(config as TCommandRemoveOptions), + from: paths.from, p: paths.tsconfig, project: paths.tsconfig, c: paths.config, @@ -29,6 +35,7 @@ export function castConfig( default: return { + from: paths.from, p: paths.tsconfig, project: paths.tsconfig, c: paths.config, diff --git a/src/configs/getConfigObject.ts b/src/configs/getConfigObject.ts new file mode 100644 index 0000000..d503b8f --- /dev/null +++ b/src/configs/getConfigObject.ts @@ -0,0 +1,17 @@ +export function getConfigObject( + argv: Record, + ...keywordArgs: string[] +): Record | undefined { + const keywords = [...keywordArgs]; + const keys = keywords.filter((keyword) => keyword in argv && argv[keyword] != null); + + if (keys.length <= 0) { + return undefined; + } + + const aggregated = keys.reduce>((obj, key) => { + return { ...obj, [key]: argv[key] }; + }, {}); + + return aggregated; +} diff --git a/src/configs/loadConfig.ts b/src/configs/loadConfig.ts index 7831d06..5806fd5 100644 --- a/src/configs/loadConfig.ts +++ b/src/configs/loadConfig.ts @@ -1,103 +1,146 @@ -import { Spinner } from '#/cli/ux/Spinner'; import { castConfig } from '#/configs/castConfig'; import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; +import { getConfigObject } from '#/configs/getConfigObject'; import { getConfigValue } from '#/configs/getConfigValue'; import type { IProjectOptions } from '#/configs/interfaces/IProjectOptions'; import type { TCommandBuildArgvOptions } from '#/configs/interfaces/TCommandBuildArgvOptions'; import type { TCommandRemoveOptions } from '#/configs/interfaces/TCommandRemoveOptions'; import { getCommand } from '#/configs/modules/getCommand'; -import { readJsonConfig } from '#/configs/modules/json/readJsonConfig'; -import { parseConfig } from '#/configs/parseConfig'; -import { getConfigFilePath } from '#/modules/path/getConfigFilePath'; -import { posixJoin } from '#/modules/path/modules/posixJoin'; +import { getConfigFilePath } from '#/configs/modules/getConfigFilePath'; +import { readConfigFromFile } from '#/configs/modules/readConfigFromFile'; +import { readConfigFromPackageJson } from '#/configs/modules/readConfigFromPackageJson'; +import { readConfigFromTsconfigJson } from '#/configs/modules/readConfigFromTsconfigJson'; +import { getCheckedValue } from '#/modules/values/getCheckedValue'; import consola from 'consola'; -import findUp from 'find-up'; import minimist from 'minimist'; import { isError } from 'my-easy-fp'; -import fs from 'node:fs'; -import type { PackageJson, TsConfigJson } from 'type-fest'; export async function loadConfig(): Promise< TCommandBuildArgvOptions | TCommandRemoveOptions | IProjectOptions > { try { + const configValueKeys = [ + 'force-yes', + 'y', + 'remove-backup', + 'export-filename', + 'f', + 'output', + 'o', + 'skip-empty-dir', + 'start-from', + 'project', + 'p', + 'mode', + 'use-semicolon', + 'use-banner', + 'quote', + 'q', + 'directive', + 'file-ext', + 'overwrite', + 'w', + 'backup', + 'generation-style', + 'include-files', + 'exclude-files', + 'config', + 'c', + 'spinner-stream', + 'progress-stream', + 'reasoner-stream', + ]; const argv = minimist(process.argv.slice(2)); - const prjectPath = getConfigValue(argv, 'p', 'project'); - const tsconfigPath = - prjectPath != null - ? findUp.sync(prjectPath) - : findUp.sync(CE_CTIX_DEFAULT_VALUE.TSCONFIG_FILENAME); - - const configFilePath = getConfigFilePath(argv, tsconfigPath); + // const configFilePath = getConfigFilePath(argv, tsconfigPath); const command = getCommand(argv._); - if (tsconfigPath == null) { - Spinner.it.fail('Cannot found tsconfig.json file!'); - throw new Error('Cannot found tsconfig.json file!'); - } + const configFilePath = await getConfigFilePath( + CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME, + getConfigValue(argv, 'c', 'config'), + ); + + const tsconfigFilePath = await getConfigFilePath( + CE_CTIX_DEFAULT_VALUE.TSCONFIG_FILENAME, + getConfigValue(argv, 'p', 'project'), + ); + + const configFileEither = + configFilePath != null ? await readConfigFromFile(configFilePath) : undefined; // case 1. using .ctirc - if (configFilePath != null) { - const parsed = - configFilePath != null ? parseConfig(await fs.promises.readFile(configFilePath)) : {}; + if (configFileEither != null && configFileEither.type === 'pass') { + const projectFilePath = + getCheckedValue('String', getConfigValue(argv, 'p', 'project')) ?? + getCheckedValue('String', configFileEither.pass.p) ?? + getCheckedValue('String', configFileEither.pass.project) ?? + tsconfigFilePath; - const config = castConfig(command, parsed, { - config: configFilePath, - tsconfig: tsconfigPath, - }); + const config = castConfig( + command, + { + ...configFileEither.pass, + ...getConfigObject(argv, ...configValueKeys), + }, + { + from: '.ctirc', + config: configFilePath, + tsconfig: projectFilePath, + }, + ); - Spinner.it.fail("load configuration from '.ctirc'"); return config; } - // case 2. using tsconfig.json - const tsconfigParsed = await readJsonConfig(tsconfigPath); - if ( - configFilePath == null && - tsconfigParsed != null && - 'ctix' in tsconfigParsed && - tsconfigParsed.ctix != null && - typeof tsconfigParsed.ctix === 'object' && - Object.keys(tsconfigParsed.ctix).length > 0 - ) { - const config = castConfig(command, tsconfigParsed.ctix, { - config: configFilePath, - tsconfig: tsconfigPath, - }); + const tsconfigEither = + tsconfigFilePath != null ? await readConfigFromTsconfigJson(tsconfigFilePath) : undefined; + + if (tsconfigEither != null && tsconfigEither.type === 'pass') { + const config = castConfig( + command, + { + ...tsconfigEither.pass, + ...getConfigObject(argv, ...configValueKeys), + }, + { + from: 'tsconfig.json', + config: configFilePath, + tsconfig: tsconfigFilePath, + }, + ); - Spinner.it.fail("load configuration from 'tsconfig.json'"); return config; } - // case 3. using package.json - const packageJsonParsed = await readJsonConfig( - posixJoin(process.cwd(), 'package.json'), - ); - if ( - configFilePath == null && - packageJsonParsed != null && - 'ctix' in packageJsonParsed && - packageJsonParsed.ctix != null && - typeof packageJsonParsed.ctix === 'object' && - Object.keys(packageJsonParsed.ctix).length > 0 - ) { - const config = castConfig(command, packageJsonParsed.ctix, { - config: configFilePath, - tsconfig: tsconfigPath, - }); + const packageJsonEither = await readConfigFromPackageJson(); + + if (packageJsonEither.type === 'pass') { + const config = castConfig( + command, + { + ...packageJsonEither.pass, + ...getConfigObject(argv, ...configValueKeys), + }, + { + from: 'package.json', + config: configFilePath, + tsconfig: tsconfigFilePath, + }, + ); - Spinner.it.fail("load configuration from 'package.json'"); return config; } // case 4. in case of a read failure from .ctirc, tsconfig.json, or package.json const config = castConfig( command, - {}, { + ...getConfigObject(argv, ...configValueKeys), + }, + { + from: 'none', config: configFilePath, - tsconfig: tsconfigPath, + tsconfig: tsconfigFilePath, }, ); diff --git a/src/configs/modules/__tests__/config.file.test.ts b/src/configs/modules/__tests__/config.file.test.ts new file mode 100644 index 0000000..2669e0e --- /dev/null +++ b/src/configs/modules/__tests__/config.file.test.ts @@ -0,0 +1,194 @@ +import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; +import { getConfigFilePath } from '#/configs/modules/getConfigFilePath'; +import { readConfigFromFile } from '#/configs/modules/readConfigFromFile'; +import { readConfigFromPackageJson } from '#/configs/modules/readConfigFromPackageJson'; +import { readConfigFromTsconfigJson } from '#/configs/modules/readConfigFromTsconfigJson'; +import fs from 'fs'; +import pathe from 'pathe'; +import { assert, describe, expect, it, vitest } from 'vitest'; + +/* + * caution + * + * `process.cwd()` function have to return absolute path. + * if you return relative path, find-up exectue infinity loop + */ +describe('readConfigFilePath', () => { + it('user pass configuration arguments', async () => { + const expectFilePath = 'a'; + const configFilePath = await getConfigFilePath( + CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME, + expectFilePath, + ); + expect(configFilePath).toEqual(expectFilePath); + }); + + it('find-up configuration file', async () => { + const expectFilePath = pathe.join(process.cwd(), 'examples', 'type10'); + const spyH01 = vitest.spyOn(process, 'cwd').mockImplementation(() => expectFilePath); + const configFilePath = await getConfigFilePath(CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME); + + spyH01.mockRestore(); + + expect(configFilePath).toEqual( + pathe.join(expectFilePath, CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME), + ); + }); + + it('find-up cannot found configuration file', async () => { + const expectFilePath = pathe.join(process.cwd(), 'examples', 'type01'); + const spyH01 = vitest.spyOn(process, 'cwd').mockImplementation(() => expectFilePath); + const configFilePath = await getConfigFilePath(CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME); + + spyH01.mockRestore(); + + expect(configFilePath).toBeUndefined(); + }); +}); + +describe('readConfigFile', () => { + it('successfully read configuration file on passed file-path', async () => { + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => + Promise.resolve(Buffer.from('// i am comment on configuration file\n{"name":"hello"}')), + ); + + const config = await readConfigFromFile('config-file-path'); + + spyH01.mockRestore(); + + assert(config.type === 'pass'); + expect(config.pass).toMatchObject({ name: 'hello' }); + }); + + it('successfully read configuration file on passed file-path but invalid yaml format', async () => { + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => + Promise.resolve(Buffer.from('// test\n\n - name:hello} // aa\n- name:hello')), + ); + + const config = await readConfigFromFile('config-file-path'); + + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).toBeInstanceOf(Error); + }); + + it('fail read configuration file on passed file-path, invalid yaml format', async () => { + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => + Promise.resolve(Buffer.from(' - name:hello} // aa\n- name:hello')), + ); + + const config = await readConfigFromFile('config-file-path'); + + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).toBeInstanceOf(Error); + }); +}); + +describe('readConfigFromPackageJson', () => { + it('successfully read configuration on package.json', async () => { + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => + Promise.resolve( + Buffer.from(JSON.stringify({ name: 'ctix', ctix: { include: ['src/**/*.ts'] } })), + ), + ); + + const config = await readConfigFromPackageJson(); + spyH01.mockRestore(); + + assert(config.type === 'pass'); + expect(config.pass).toMatchObject({ include: ['src/**/*.ts'] }); + }); + + it('fail read configuration on package.json', async () => { + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => Promise.resolve(Buffer.from(JSON.stringify({ name: 'ctix' })))); + + const config = await readConfigFromPackageJson(); + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).instanceOf(Error); + }); + + it('fail read configuration on package.json', async () => { + const spyH01 = vitest.spyOn(fs.promises, 'readFile').mockImplementation(() => { + throw new Error('raise error by testcase'); + }); + + const config = await readConfigFromPackageJson(); + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).instanceOf(Error); + }); +}); + +describe('readConfigFromTsconfigJson', () => { + it('successfully read configuration on tsconfig.json', async () => { + const filePath = pathe.join(process.cwd(), 'tsconfig.json'); + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => + Promise.resolve( + Buffer.from(JSON.stringify({ name: 'ctix', ctix: { include: ['src/**/*.ts'] } })), + ), + ); + + const config = await readConfigFromTsconfigJson(filePath); + spyH01.mockRestore(); + + assert(config.type === 'pass'); + expect(config.pass).toMatchObject({ include: ['src/**/*.ts'] }); + }); + + it('fail read configuration on tsconfig.json, because invalid json content', async () => { + const filePath = pathe.join(process.cwd(), 'tsconfig.json'); + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => Promise.resolve(Buffer.from('{ invalid json contents'))); + + const config = await readConfigFromTsconfigJson(filePath); + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).instanceOf(Error); + }); + + it('fail read configuration on tsconfig.json, because not found configuration in tsconfig.json', async () => { + const filePath = pathe.join(process.cwd(), 'tsconfig.json'); + const spyH01 = vitest + .spyOn(fs.promises, 'readFile') + .mockImplementation(() => Promise.resolve(Buffer.from(JSON.stringify({ name: 'ctix' })))); + + const config = await readConfigFromTsconfigJson(filePath); + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).instanceOf(Error); + }); + + it('fail read configuration on tsconfig.json', async () => { + const filePath = pathe.join(process.cwd(), 'tsconfig.json'); + const spyH01 = vitest.spyOn(fs.promises, 'readFile').mockImplementation(() => { + throw new Error('raise error by testcase'); + }); + + const config = await readConfigFromTsconfigJson(filePath); + spyH01.mockRestore(); + + assert(config.type === 'fail'); + expect(config.fail).instanceOf(Error); + }); +}); diff --git a/src/configs/modules/getConfigFilePath.ts b/src/configs/modules/getConfigFilePath.ts new file mode 100644 index 0000000..698b4fb --- /dev/null +++ b/src/configs/modules/getConfigFilePath.ts @@ -0,0 +1,16 @@ +import { exists } from 'my-node-fp'; +import pathe from 'pathe'; + +export async function getConfigFilePath(fileName: string, configFilePath?: string) { + if (configFilePath != null) { + return configFilePath; + } + + const cwdConfigFilePath = pathe.join(process.cwd(), fileName); + + if (await exists(cwdConfigFilePath)) { + return cwdConfigFilePath; + } + + return undefined; +} diff --git a/src/configs/modules/readConfigFromFile.ts b/src/configs/modules/readConfigFromFile.ts new file mode 100644 index 0000000..9a10fd0 --- /dev/null +++ b/src/configs/modules/readConfigFromFile.ts @@ -0,0 +1,22 @@ +import { parseConfig } from '#/configs/parseConfig'; +import fs from 'fs'; +import { isError } from 'my-easy-fp'; +import { type PassFailEither, fail, pass } from 'my-only-either'; + +export async function readConfigFromFile( + configFilePath: string, +): Promise>> { + try { + const buf = await fs.promises.readFile(configFilePath); + const parsed = parseConfig>(buf); + + if (typeof parsed !== 'object') { + return fail(new Error(`invalid configuration file format: ${parsed as string}`)); + } + + return pass(parsed); + } catch (caught) { + const err = isError(caught, new Error('unknown error raised from configuration reading')); + return fail(err); + } +} diff --git a/src/configs/modules/readConfigFromPackageJson.ts b/src/configs/modules/readConfigFromPackageJson.ts new file mode 100644 index 0000000..6ca301b --- /dev/null +++ b/src/configs/modules/readConfigFromPackageJson.ts @@ -0,0 +1,30 @@ +import fs from 'fs'; +import { isError } from 'my-easy-fp'; +import { type PassFailEither, fail, pass } from 'my-only-either'; +import pathe from 'pathe'; +import type { PackageJson } from 'type-fest'; + +export async function readConfigFromPackageJson(): Promise< + PassFailEither> +> { + try { + const packageJsonFilePath = pathe.join(process.cwd(), 'package.json'); + const buf = await fs.promises.readFile(packageJsonFilePath); + const packageJson = JSON.parse(buf.toString()) as PackageJson; + + if ( + 'ctix' in packageJson && + typeof packageJson.ctix === 'object' && + packageJson.ctix != null && + Object.keys(packageJson.ctix).length > 0 + ) { + const config = packageJson.ctix as Record; + return pass(config); + } + + return fail(new Error('cannot read configuration from package.json')); + } catch (caught) { + const err = isError(caught, new Error('unknown error raised from configuration reading')); + return fail(err); + } +} diff --git a/src/configs/modules/readConfigFromTsconfigJson.ts b/src/configs/modules/readConfigFromTsconfigJson.ts new file mode 100644 index 0000000..47adb14 --- /dev/null +++ b/src/configs/modules/readConfigFromTsconfigJson.ts @@ -0,0 +1,34 @@ +import { readJsonc } from '#/configs/modules/json/readJsonc'; +import fs from 'fs'; +import { isError } from 'my-easy-fp'; +import { type PassFailEither, fail, pass } from 'my-only-either'; + +export async function readConfigFromTsconfigJson( + tsconfigFilePath: string, +): Promise>> { + try { + const buf = await fs.promises.readFile(tsconfigFilePath); + const parsed = readJsonc>(buf); + + if (parsed.type === 'fail') { + return parsed; + } + + const tsconfig = parsed.pass; + + if ( + 'ctix' in tsconfig && + typeof tsconfig.ctix === 'object' && + tsconfig.ctix != null && + Object.keys(tsconfig.ctix).length > 0 + ) { + const config = tsconfig.ctix as Record; + return pass(config); + } + + return fail(new Error(`cannot read configuration from ${tsconfigFilePath}`)); + } catch (caught) { + const err = isError(caught, new Error('unknown error raised from configuration reading')); + return fail(err); + } +} diff --git a/src/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index d1ece70..9fefda6 100644 --- a/src/configs/transforms/createBuildOptions.ts +++ b/src/configs/transforms/createBuildOptions.ts @@ -21,17 +21,23 @@ import type { ArgumentsCamelCase } from 'yargs'; export async function createBuildOptions( argv: ArgumentsCamelCase & { options?: (TCreateOptions | TBundleOptions)[]; + from?: string; }, ): Promise { - const options: TCommandBuildOptions = { + const options: TCommandBuildOptions & { from: string } = { $kind: CE_CTIX_COMMAND.BUILD_COMMAND, config: argv.config, + from: argv.from ?? 'none', spinnerStream: argv.spinnerStream, progressStream: argv.progressStream, reasonerStream: argv.reasonerStream, options: [], }; + if ('from' in argv && argv.from != null && typeof argv.from === 'string') { + options.from = argv.from; + } + Spinner.it.stream = argv.spinnerStream; ProgressBar.it.stream = argv.progressStream; Reasoner.it.stream = argv.reasonerStream; diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index c619d65..7a8a125 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -36,9 +36,18 @@ import { replaceSepToPosix } from 'my-node-fp'; import path from 'node:path'; import type * as tsm from 'ts-morph'; -export async function bundling(_buildOptions: TCommandBuildOptions, bundleOption: TBundleOptions) { +export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: TBundleOptions) { Spinner.it.start("ctix 'bundle' mode start, ..."); + if ( + 'from' in buildOptions && + buildOptions.from && + typeof buildOptions.from === 'string' && + buildOptions.from !== 'none' + ) { + Spinner.it.succeed(`ctix 'bundle' mode configuration reading from '${buildOptions.from}'`); + } + await TemplateContainer.bootstrap(); const extendOptions = await getExtendOptions(bundleOption.project); diff --git a/src/modules/path/__tests__/config.path.module.test.ts b/src/modules/path/__tests__/config.path.module.test.ts index fc4ab18..d66c00d 100644 --- a/src/modules/path/__tests__/config.path.module.test.ts +++ b/src/modules/path/__tests__/config.path.module.test.ts @@ -1,7 +1,4 @@ import { getConfigValue } from '#/configs/getConfigValue'; -import { getConfigFilePath } from '#/modules/path/getConfigFilePath'; -import findUp from 'find-up'; -import * as mnf from 'my-node-fp'; import { describe, expect, it, vitest } from 'vitest'; vitest.mock('my-node-fp', async (importOriginal) => { @@ -56,101 +53,3 @@ describe('getConfigValue', () => { expect(data).toBeUndefined(); }); }); - -describe('getConfigFilePath', () => { - it('config path from argv', () => { - const spyH = vitest.spyOn(mnf, 'existsSync').mockImplementationOnce(() => true); - - const configPath = 'i-am-config'; - const input = { $0: '', config: configPath }; - const c = getConfigFilePath(input); - - spyH.mockRestore(); - - expect(c).toEqual(configPath); - }); - - it('config path from process.cwd() with find-up', () => { - const findUpPath = 'i-am-findUp'; - const spyH01 = vitest - .spyOn(mnf, 'existsSync') - .mockImplementationOnce(() => false) - .mockImplementationOnce(() => true); - const spyH02 = vitest.spyOn(findUp, 'sync').mockImplementationOnce(() => findUpPath); - - const configPath = 'i-am-config'; - const input = { $0: '', config: configPath }; - const c = getConfigFilePath(input); - - spyH01.mockRestore(); - spyH02.mockRestore(); - - expect(c).toEqual(findUpPath); - }); - - it('config path from project-path with find-up', () => { - const findUpPath = 'i-am-findUp'; - const spyH01 = vitest - .spyOn(mnf, 'existsSync') - .mockImplementationOnce(() => false) - .mockImplementationOnce(() => true); - const spyH02 = vitest - .spyOn(findUp, 'sync') - .mockImplementationOnce(() => undefined) - .mockImplementationOnce(() => findUpPath); - - const configPath = 'i-am-config'; - const projectPath = 'i-am-project'; - const input = { $0: '', config: configPath }; - - const c = getConfigFilePath(input, projectPath); - - spyH01.mockRestore(); - spyH02.mockRestore(); - - expect(c).toEqual(findUpPath); - }); - - it('undefined - not founded, empty project path', () => { - const spyH01 = vitest - .spyOn(mnf, 'existsSync') - .mockImplementationOnce(() => false) - .mockImplementationOnce(() => true); - const spyH02 = vitest - .spyOn(findUp, 'sync') - .mockImplementationOnce(() => undefined) - .mockImplementationOnce(() => undefined); - - const configPath = 'i-am-config'; - const input = { $0: '', config: configPath }; - - const c = getConfigFilePath(input); - - spyH01.mockRestore(); - spyH02.mockRestore(); - - expect(c).toBeUndefined(); - }); - - it('undefined - not founded, pass project path', () => { - const spyH01 = vitest - .spyOn(mnf, 'existsSync') - .mockImplementationOnce(() => false) - .mockImplementationOnce(() => true); - const spyH02 = vitest - .spyOn(findUp, 'sync') - .mockImplementationOnce(() => undefined) - .mockImplementationOnce(() => undefined); - - const configPath = 'i-am-config'; - const projectPath = 'i-am-project'; - const input = { $0: '', config: configPath }; - - const c = getConfigFilePath(input, projectPath); - - spyH01.mockRestore(); - spyH02.mockRestore(); - - expect(c).toBeUndefined(); - }); -}); diff --git a/src/modules/path/getConfigFilePath.ts b/src/modules/path/getConfigFilePath.ts deleted file mode 100644 index 9a7f4a6..0000000 --- a/src/modules/path/getConfigFilePath.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; -import { getConfigValue } from '#/configs/getConfigValue'; -import findUp from 'find-up'; -import { existsSync, getDirnameSync } from 'my-node-fp'; - -export function getConfigFilePath(argv: Record, projectPath?: string) { - const fromArgv = getConfigValue(argv, 'c', 'config'); - - if (fromArgv != null && existsSync(fromArgv)) { - return fromArgv; - } - - const fromSearchOnResultOnCwd = findUp.sync(CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME); - - if (fromSearchOnResultOnCwd != null && existsSync(fromSearchOnResultOnCwd)) { - return fromSearchOnResultOnCwd; - } - - const projectDirPath = projectPath != null ? getDirnameSync(projectPath) : undefined; - const fromSearchOnProjectDir = - projectDirPath != null - ? findUp.sync(CE_CTIX_DEFAULT_VALUE.CONFIG_FILENAME, { cwd: projectDirPath }) - : undefined; - - if (fromSearchOnProjectDir != null && existsSync(fromSearchOnProjectDir)) { - return fromSearchOnProjectDir; - } - - return undefined; -} diff --git a/src/modules/values/__tests__/valid.type.test.ts b/src/modules/values/__tests__/valid.type.test.ts new file mode 100644 index 0000000..e1efc9a --- /dev/null +++ b/src/modules/values/__tests__/valid.type.test.ts @@ -0,0 +1,20 @@ +import { getCheckedValue } from '#/modules/values/getCheckedValue'; +import { describe, expect, it } from 'vitest'; + +describe('getValidValue', () => { + it('variety case', () => { + const r01 = getCheckedValue('Number', 1); + const r02 = getCheckedValue('Number', '1'); + const r03 = getCheckedValue('String', 'hello'); + const r04 = getCheckedValue('String', 1); + const r05 = getCheckedValue('String', undefined); + + console.log(r01, r02, r03, r04); + + expect(r01).toEqual(1); + expect(r02).toBeUndefined(); + expect(r03).toEqual('hello'); + expect(r04).toBeUndefined(); + expect(r05).toBeUndefined(); + }); +}); diff --git a/src/modules/values/getCheckedValue.ts b/src/modules/values/getCheckedValue.ts new file mode 100644 index 0000000..3973cf6 --- /dev/null +++ b/src/modules/values/getCheckedValue.ts @@ -0,0 +1,11 @@ +import { typeCheck } from 'type-check'; + +export function getCheckedValue(types: string, value: unknown): T | undefined { + const checked = typeCheck(types, value); + + if (checked) { + return value as T; + } + + return undefined; +}