@wato787/microcms-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/commands/gen-types/command.js +105 -0
- package/dist/commands/gen-types/config.js +34 -0
- package/dist/commands/gen-types/constants.js +3 -0
- package/dist/commands/gen-types/index.js +1 -0
- package/dist/commands/gen-types/management-api.js +131 -0
- package/dist/commands/gen-types/shared.js +36 -0
- package/dist/commands/gen-types/type-generator.js +182 -0
- package/dist/commands/gen-types/types.js +1 -0
- package/dist/index.js +23 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wato787
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# microCMS CLI
|
|
2
|
+
|
|
3
|
+
microCMS 用の CLI ツールです。実行コマンドは `microcms-cli` です。
|
|
4
|
+
npm 配布物は事前ビルド済みのため、実行時に Bun は不要です(Node.js 18+)。
|
|
5
|
+
|
|
6
|
+
- `gen-types`: Management API のスキーマから TypeScript 型を生成
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @wato787/microcms-cli
|
|
12
|
+
# または
|
|
13
|
+
bun add -g @wato787/microcms-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# インストールせずに実行
|
|
18
|
+
npx @wato787/microcms-cli gen-types blog
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
### `gen-types [endpointId] [options]`
|
|
24
|
+
|
|
25
|
+
microCMS Management API(`/api/v1/apis`)から API スキーマを取得し、TypeScript 型を生成します。
|
|
26
|
+
生成結果は `microcms.d.ts` に集約されます。
|
|
27
|
+
共通型(`MicroCMSListResponse` など)は `microcms-js-sdk` の公式定義に合わせています。
|
|
28
|
+
このコマンドは Content API のコンテンツ取得エンドポイントは呼びません。
|
|
29
|
+
|
|
30
|
+
- `endpointId` 指定時: 対象 endpoint のスキーマのみ取得
|
|
31
|
+
- `--all` 指定時: API 一覧を取得して全 endpoint を生成
|
|
32
|
+
- 生成型: `XxxSchema`(スキーマ本体)と `XxxContent`(SDK の共通メタ情報付き)
|
|
33
|
+
- 利用する Management API: `/api/v1/apis`, `/api/v1/apis/{endpoint}`
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 単一エンドポイント
|
|
37
|
+
microcms-cli gen-types blog
|
|
38
|
+
|
|
39
|
+
# 出力先ディレクトリ指定(デフォルトは ./types/microcms.d.ts)
|
|
40
|
+
microcms-cli gen-types blog -o ./src/types/microcms
|
|
41
|
+
# => ./src/types/microcms/microcms.d.ts
|
|
42
|
+
|
|
43
|
+
# 出力ファイルを直接指定
|
|
44
|
+
microcms-cli gen-types blog -o ./src/types/microcms.d.ts
|
|
45
|
+
|
|
46
|
+
# 全エンドポイントを一括生成
|
|
47
|
+
microcms-cli gen-types --all
|
|
48
|
+
# endpointIdを渡しても --all 指定時は無視されます
|
|
49
|
+
microcms-cli gen-types blog --all
|
|
50
|
+
|
|
51
|
+
# CIなどで環境変数をインライン指定
|
|
52
|
+
MICROCMS_SERVICE_DOMAIN=your-service-id \
|
|
53
|
+
MICROCMS_MANAGEMENT_API_KEY=your-management-api-key \
|
|
54
|
+
microcms-cli gen-types blog
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Options
|
|
58
|
+
|
|
59
|
+
- `-o, --output <path>`: 出力先(ディレクトリ指定時は `<path>/microcms.d.ts`、デフォルト `./types/microcms.d.ts`)
|
|
60
|
+
- `--all`: 全エンドポイントの型を生成
|
|
61
|
+
- `--service-domain <domain>`: `MICROCMS_SERVICE_DOMAIN` をCLI引数で上書き
|
|
62
|
+
- `--api-key <key>`: `MICROCMS_MANAGEMENT_API_KEY` をCLI引数で上書き
|
|
63
|
+
|
|
64
|
+
#### Required environment variables
|
|
65
|
+
|
|
66
|
+
`gen-types` は **実行した利用者の環境変数** から設定を読み取ります。
|
|
67
|
+
CLI引数で指定しない場合、以下の環境変数が必要です。
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
MICROCMS_SERVICE_DOMAIN=your-service-id
|
|
71
|
+
MICROCMS_MANAGEMENT_API_KEY=your-management-api-key
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Required Management API permission
|
|
75
|
+
|
|
76
|
+
`MICROCMS_MANAGEMENT_API_KEY` には、マネジメントAPIのGET権限として **「API情報の取得」** が必要です。
|
|
77
|
+
|
|
78
|
+
> 補足: 単一 endpoint で `apiType` が取得できない場合、CLI は警告を出して LIST として型生成します。
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bun install
|
|
84
|
+
npm run build
|
|
85
|
+
bun run typecheck
|
|
86
|
+
bun run test
|
|
87
|
+
|
|
88
|
+
# 事前ビルド済み CLI を実行
|
|
89
|
+
npm run start -- gen-types blog
|
|
90
|
+
|
|
91
|
+
# ソースから実行(Bun)
|
|
92
|
+
bun run dev gen-types blog
|
|
93
|
+
|
|
94
|
+
# 単一バイナリを生成
|
|
95
|
+
bun run compile
|
|
96
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveConfig, resolveOutputFilePath, resolveSingleEndpoint } from './config.js';
|
|
4
|
+
import { fetchApiList, fetchApiSchema } from './management-api.js';
|
|
5
|
+
import { toErrorMessage } from './shared.js';
|
|
6
|
+
import { renderDefinitionsFile } from './type-generator.js';
|
|
7
|
+
function getTargetEndpoints(apiList) {
|
|
8
|
+
const deduped = new Map();
|
|
9
|
+
for (const api of apiList) {
|
|
10
|
+
if (!deduped.has(api.apiEndpoint)) {
|
|
11
|
+
deduped.set(api.apiEndpoint, api);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return Array.from(deduped.values()).sort((a, b) => a.apiEndpoint.localeCompare(b.apiEndpoint));
|
|
15
|
+
}
|
|
16
|
+
function createGenerationTarget(endpoint, schema, apiType) {
|
|
17
|
+
return {
|
|
18
|
+
endpoint,
|
|
19
|
+
apiType,
|
|
20
|
+
schema: {
|
|
21
|
+
...schema,
|
|
22
|
+
apiEndpoint: schema.apiEndpoint ?? endpoint,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function resolveApiTypeByEndpoint(apiList, endpoint) {
|
|
27
|
+
return apiList.find((item) => item.apiEndpoint === endpoint)?.apiType;
|
|
28
|
+
}
|
|
29
|
+
async function resolveSingleEndpointApiType(config, endpoint, schema, fetchApiListFn, warn) {
|
|
30
|
+
if (schema.apiType) {
|
|
31
|
+
return schema.apiType;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const apiList = await fetchApiListFn(config);
|
|
35
|
+
return resolveApiTypeByEndpoint(apiList, endpoint);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
warn(`[warn] Failed to resolve apiType for "${endpoint}" from /apis: ${toErrorMessage(error)}`);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const defaultGenTypesCommandDeps = {
|
|
43
|
+
resolveOutputFilePath,
|
|
44
|
+
resolveConfig,
|
|
45
|
+
resolveSingleEndpoint,
|
|
46
|
+
fetchApiList,
|
|
47
|
+
fetchApiSchema,
|
|
48
|
+
renderDefinitionsFile,
|
|
49
|
+
mkdirSync: fs.mkdirSync,
|
|
50
|
+
writeFileSync: fs.writeFileSync,
|
|
51
|
+
warn: console.warn,
|
|
52
|
+
log: console.log,
|
|
53
|
+
error: console.error,
|
|
54
|
+
exit: process.exit,
|
|
55
|
+
};
|
|
56
|
+
async function runGenTypesCommand(endpointId, options, deps) {
|
|
57
|
+
try {
|
|
58
|
+
const all = Boolean(options.all);
|
|
59
|
+
if (all && endpointId) {
|
|
60
|
+
deps.warn('[warn] endpointId is ignored because --all is specified.');
|
|
61
|
+
}
|
|
62
|
+
const outputFilePath = deps.resolveOutputFilePath(options.output);
|
|
63
|
+
const config = deps.resolveConfig(options);
|
|
64
|
+
deps.mkdirSync(path.dirname(outputFilePath), { recursive: true });
|
|
65
|
+
const targets = [];
|
|
66
|
+
if (all) {
|
|
67
|
+
const apiList = getTargetEndpoints(await deps.fetchApiList(config));
|
|
68
|
+
if (apiList.length === 0) {
|
|
69
|
+
throw new Error('No endpoints found from Management API.');
|
|
70
|
+
}
|
|
71
|
+
for (const api of apiList) {
|
|
72
|
+
const schema = await deps.fetchApiSchema(config, api.apiEndpoint);
|
|
73
|
+
targets.push(createGenerationTarget(api.apiEndpoint, schema, api.apiType));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const endpoint = deps.resolveSingleEndpoint(endpointId);
|
|
78
|
+
const schema = await deps.fetchApiSchema(config, endpoint);
|
|
79
|
+
const apiType = await resolveSingleEndpointApiType(config, endpoint, schema, deps.fetchApiList, deps.warn);
|
|
80
|
+
if (!apiType && !schema.apiType) {
|
|
81
|
+
deps.warn(`[warn] apiType for "${endpoint}" could not be resolved. Falling back to LIST.`);
|
|
82
|
+
}
|
|
83
|
+
targets.push(createGenerationTarget(endpoint, schema, apiType));
|
|
84
|
+
}
|
|
85
|
+
const source = deps.renderDefinitionsFile(targets);
|
|
86
|
+
deps.writeFileSync(outputFilePath, source, 'utf-8');
|
|
87
|
+
deps.log(`Generated ${targets.length} endpoint type(s): ${outputFilePath}`);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
deps.error('Error:', toErrorMessage(error));
|
|
91
|
+
deps.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createGenTypesCommand(overrides = {}) {
|
|
95
|
+
const deps = {
|
|
96
|
+
...defaultGenTypesCommandDeps,
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
return async (endpointId, options) => {
|
|
100
|
+
await runGenTypesCommand(endpointId, options, deps);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function genTypesCommand(endpointId, options) {
|
|
104
|
+
await runGenTypesCommand(endpointId, options, defaultGenTypesCommandDeps);
|
|
105
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { DEFAULT_OUTPUT_FILE_NAME, DEFAULT_OUTPUT_PATH } from './constants.js';
|
|
3
|
+
import { toStringValue } from './shared.js';
|
|
4
|
+
function isDeclarationFilePath(filePath) {
|
|
5
|
+
return filePath.toLowerCase().endsWith('.d.ts');
|
|
6
|
+
}
|
|
7
|
+
export function resolveOutputFilePath(outputOption) {
|
|
8
|
+
const outputPath = outputOption ?? DEFAULT_OUTPUT_PATH;
|
|
9
|
+
const resolvedPath = path.resolve(process.cwd(), outputPath);
|
|
10
|
+
if (isDeclarationFilePath(resolvedPath)) {
|
|
11
|
+
return resolvedPath;
|
|
12
|
+
}
|
|
13
|
+
return path.join(resolvedPath, DEFAULT_OUTPUT_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
export function resolveConfig(options) {
|
|
16
|
+
const serviceDomain = toStringValue(options.serviceDomain) ??
|
|
17
|
+
toStringValue(process.env.MICROCMS_SERVICE_DOMAIN);
|
|
18
|
+
if (!serviceDomain) {
|
|
19
|
+
throw new Error('MICROCMS_SERVICE_DOMAIN is required. You can also pass --service-domain.');
|
|
20
|
+
}
|
|
21
|
+
const apiKey = toStringValue(options.apiKey) ??
|
|
22
|
+
toStringValue(process.env.MICROCMS_MANAGEMENT_API_KEY);
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error('MICROCMS_MANAGEMENT_API_KEY is required. You can also pass --api-key.');
|
|
25
|
+
}
|
|
26
|
+
return { serviceDomain, apiKey };
|
|
27
|
+
}
|
|
28
|
+
export function resolveSingleEndpoint(endpointId) {
|
|
29
|
+
const resolved = toStringValue(endpointId);
|
|
30
|
+
if (!resolved) {
|
|
31
|
+
throw new Error('endpointId is required unless --all is specified.');
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { genTypesCommand } from './command.js';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { MANAGEMENT_API_BASE_DOMAIN } from './constants.js';
|
|
2
|
+
import { isRecord, toBooleanValue, toStringArray, toStringValue, } from './shared.js';
|
|
3
|
+
function buildManagementApiUrl(serviceDomain, resourcePath) {
|
|
4
|
+
return `https://${serviceDomain}.${MANAGEMENT_API_BASE_DOMAIN}/api/v1/${resourcePath}`;
|
|
5
|
+
}
|
|
6
|
+
async function fetchFromManagementApi(url, apiKey) {
|
|
7
|
+
const response = await fetch(url, {
|
|
8
|
+
method: 'GET',
|
|
9
|
+
headers: {
|
|
10
|
+
'X-MICROCMS-API-KEY': apiKey,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const responseBody = await response.text();
|
|
15
|
+
if (response.status === 401) {
|
|
16
|
+
throw new Error(`Management API authentication failed (401). Check MICROCMS_MANAGEMENT_API_KEY. ${responseBody}`);
|
|
17
|
+
}
|
|
18
|
+
if (response.status === 403) {
|
|
19
|
+
throw new Error(`Management API authorization failed (403). Ensure "API情報の取得" is enabled for the key. ${responseBody}`);
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Management API request failed (${response.status} ${response.statusText}): ${responseBody}`);
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
25
|
+
function parseApiField(rawField) {
|
|
26
|
+
if (!isRecord(rawField)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const fieldId = toStringValue(rawField.fieldId);
|
|
30
|
+
const kind = toStringValue(rawField.kind);
|
|
31
|
+
if (!fieldId || !kind) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
fieldId,
|
|
36
|
+
kind,
|
|
37
|
+
required: toBooleanValue(rawField.required) ?? false,
|
|
38
|
+
multipleSelect: toBooleanValue(rawField.multipleSelect) ?? false,
|
|
39
|
+
customFieldCreatedAt: toStringValue(rawField.customFieldCreatedAt),
|
|
40
|
+
customFieldCreatedAtList: toStringArray(rawField.customFieldCreatedAtList),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseCustomField(rawCustomField) {
|
|
44
|
+
if (!isRecord(rawCustomField)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const createdAt = toStringValue(rawCustomField.createdAt);
|
|
48
|
+
const fieldId = toStringValue(rawCustomField.fieldId);
|
|
49
|
+
const fields = Array.isArray(rawCustomField.fields)
|
|
50
|
+
? rawCustomField.fields
|
|
51
|
+
.map((field) => parseApiField(field))
|
|
52
|
+
.filter((field) => field !== null)
|
|
53
|
+
: [];
|
|
54
|
+
if (!createdAt || !fieldId) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
createdAt,
|
|
59
|
+
fieldId,
|
|
60
|
+
fields,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseApiSchema(rawSchema) {
|
|
64
|
+
if (!isRecord(rawSchema)) {
|
|
65
|
+
throw new Error('Management API schema response is invalid.');
|
|
66
|
+
}
|
|
67
|
+
const apiFields = Array.isArray(rawSchema.apiFields)
|
|
68
|
+
? rawSchema.apiFields
|
|
69
|
+
.map((field) => parseApiField(field))
|
|
70
|
+
.filter((field) => field !== null)
|
|
71
|
+
: [];
|
|
72
|
+
const customFields = Array.isArray(rawSchema.customFields)
|
|
73
|
+
? rawSchema.customFields
|
|
74
|
+
.map((field) => parseCustomField(field))
|
|
75
|
+
.filter((field) => field !== null)
|
|
76
|
+
: [];
|
|
77
|
+
return {
|
|
78
|
+
apiFields,
|
|
79
|
+
customFields,
|
|
80
|
+
apiType: toStringValue(rawSchema.apiType),
|
|
81
|
+
apiEndpoint: toStringValue(rawSchema.apiEndpoint),
|
|
82
|
+
apiName: toStringValue(rawSchema.apiName),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseApiListItem(rawItem) {
|
|
86
|
+
if (!isRecord(rawItem)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const apiEndpoint = toStringValue(rawItem.apiEndpoint) ?? toStringValue(rawItem.endpoint);
|
|
90
|
+
if (!apiEndpoint) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
apiEndpoint,
|
|
95
|
+
apiName: toStringValue(rawItem.apiName) ?? toStringValue(rawItem.name),
|
|
96
|
+
apiType: toStringValue(rawItem.apiType) ?? toStringValue(rawItem.type),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseApiList(rawResponse) {
|
|
100
|
+
const candidateArrays = [];
|
|
101
|
+
if (Array.isArray(rawResponse)) {
|
|
102
|
+
candidateArrays.push(rawResponse);
|
|
103
|
+
}
|
|
104
|
+
else if (isRecord(rawResponse)) {
|
|
105
|
+
for (const key of ['apis', 'contents', 'items', 'data']) {
|
|
106
|
+
const value = rawResponse[key];
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
candidateArrays.push(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const items of candidateArrays) {
|
|
113
|
+
const parsedItems = items
|
|
114
|
+
.map((item) => parseApiListItem(item))
|
|
115
|
+
.filter((item) => item !== null);
|
|
116
|
+
if (parsedItems.length > 0) {
|
|
117
|
+
return parsedItems;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Failed to parse API list from Management API response.');
|
|
121
|
+
}
|
|
122
|
+
export async function fetchApiList(config) {
|
|
123
|
+
const url = buildManagementApiUrl(config.serviceDomain, 'apis');
|
|
124
|
+
const rawResponse = await fetchFromManagementApi(url, config.apiKey);
|
|
125
|
+
return parseApiList(rawResponse);
|
|
126
|
+
}
|
|
127
|
+
export async function fetchApiSchema(config, endpointId) {
|
|
128
|
+
const url = buildManagementApiUrl(config.serviceDomain, `apis/${encodeURIComponent(endpointId)}`);
|
|
129
|
+
const rawResponse = await fetchFromManagementApi(url, config.apiKey);
|
|
130
|
+
return parseApiSchema(rawResponse);
|
|
131
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function toStringValue(value) {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function toBooleanValue(value) {
|
|
12
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function toStringArray(value) {
|
|
15
|
+
if (!Array.isArray(value)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const values = value
|
|
19
|
+
.map((item) => toStringValue(item))
|
|
20
|
+
.filter((item) => item !== undefined);
|
|
21
|
+
return values.length > 0 ? values : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function toPascalCase(value) {
|
|
24
|
+
const chunks = value.split(/[^A-Za-z0-9]+/).filter((chunk) => chunk.length > 0);
|
|
25
|
+
const raw = chunks
|
|
26
|
+
.map((chunk) => `${chunk.charAt(0).toUpperCase()}${chunk.slice(1)}`)
|
|
27
|
+
.join('');
|
|
28
|
+
const safe = raw.length > 0 ? raw : 'Generated';
|
|
29
|
+
return /^[A-Za-z_$]/.test(safe) ? safe : `T${safe}`;
|
|
30
|
+
}
|
|
31
|
+
export function toTypePropertyName(value) {
|
|
32
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
export function toErrorMessage(error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { toPascalCase, toTypePropertyName } from './shared.js';
|
|
2
|
+
export function normalizeApiType(apiType) {
|
|
3
|
+
return apiType?.toUpperCase() === 'OBJECT' ? 'OBJECT' : 'LIST';
|
|
4
|
+
}
|
|
5
|
+
function getUniqueTypeName(baseName, context) {
|
|
6
|
+
let candidate = baseName;
|
|
7
|
+
let suffix = 2;
|
|
8
|
+
while (context.usedCustomTypeNames.has(candidate)) {
|
|
9
|
+
candidate = `${baseName}${suffix}`;
|
|
10
|
+
suffix += 1;
|
|
11
|
+
}
|
|
12
|
+
context.usedCustomTypeNames.add(candidate);
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
function resolveCustomFieldTypeName(createdAt, context) {
|
|
16
|
+
const existing = context.customTypeNameByCreatedAt.get(createdAt);
|
|
17
|
+
if (existing) {
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
const customField = context.customFieldByCreatedAt.get(createdAt);
|
|
21
|
+
if (!customField) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const baseName = `${context.endpointBaseTypeName}${toPascalCase(customField.fieldId)}CustomField`;
|
|
25
|
+
const uniqueName = getUniqueTypeName(baseName, context);
|
|
26
|
+
context.customTypeNameByCreatedAt.set(createdAt, uniqueName);
|
|
27
|
+
return uniqueName;
|
|
28
|
+
}
|
|
29
|
+
function emitCustomFieldType(createdAt, context) {
|
|
30
|
+
const typeName = resolveCustomFieldTypeName(createdAt, context);
|
|
31
|
+
if (!typeName) {
|
|
32
|
+
return 'Record<string, unknown>';
|
|
33
|
+
}
|
|
34
|
+
if (context.emittedCustomTypeNames.has(typeName)) {
|
|
35
|
+
return typeName;
|
|
36
|
+
}
|
|
37
|
+
if (context.processingCustomFieldIds.has(createdAt)) {
|
|
38
|
+
return 'Record<string, unknown>';
|
|
39
|
+
}
|
|
40
|
+
const customField = context.customFieldByCreatedAt.get(createdAt);
|
|
41
|
+
if (!customField) {
|
|
42
|
+
return 'Record<string, unknown>';
|
|
43
|
+
}
|
|
44
|
+
context.processingCustomFieldIds.add(createdAt);
|
|
45
|
+
const fieldLines = customField.fields.map((field) => renderTypePropertyLine(field, context));
|
|
46
|
+
const typeBlock = fieldLines.length === 0
|
|
47
|
+
? `export type ${typeName} = Record<string, never>;`
|
|
48
|
+
: [`export type ${typeName} = {`, ...fieldLines, '};'].join('\n');
|
|
49
|
+
context.emittedCustomTypeBlocks.push(typeBlock);
|
|
50
|
+
context.emittedCustomTypeNames.add(typeName);
|
|
51
|
+
context.processingCustomFieldIds.delete(createdAt);
|
|
52
|
+
return typeName;
|
|
53
|
+
}
|
|
54
|
+
function resolveFieldType(field, context) {
|
|
55
|
+
switch (field.kind) {
|
|
56
|
+
case 'text':
|
|
57
|
+
case 'textArea':
|
|
58
|
+
case 'richEditor':
|
|
59
|
+
case 'richEditorV2':
|
|
60
|
+
case 'date':
|
|
61
|
+
return 'string';
|
|
62
|
+
case 'number':
|
|
63
|
+
return 'number';
|
|
64
|
+
case 'boolean':
|
|
65
|
+
return 'boolean';
|
|
66
|
+
case 'media':
|
|
67
|
+
return 'MicroCMSImage';
|
|
68
|
+
case 'mediaList':
|
|
69
|
+
return 'MicroCMSImage[]';
|
|
70
|
+
case 'relation':
|
|
71
|
+
return 'MicroCMSContentId';
|
|
72
|
+
case 'relationList':
|
|
73
|
+
return 'MicroCMSContentId[]';
|
|
74
|
+
case 'select':
|
|
75
|
+
return field.multipleSelect ? 'string[]' : 'string';
|
|
76
|
+
case 'custom':
|
|
77
|
+
if (!field.customFieldCreatedAt) {
|
|
78
|
+
return 'Record<string, unknown>';
|
|
79
|
+
}
|
|
80
|
+
return emitCustomFieldType(field.customFieldCreatedAt, context);
|
|
81
|
+
case 'repeater': {
|
|
82
|
+
const customFieldCreatedAtList = field.customFieldCreatedAtList ?? [];
|
|
83
|
+
if (customFieldCreatedAtList.length === 0) {
|
|
84
|
+
return 'Array<Record<string, unknown>>';
|
|
85
|
+
}
|
|
86
|
+
const itemTypes = customFieldCreatedAtList.map((createdAt) => {
|
|
87
|
+
const customField = context.customFieldByCreatedAt.get(createdAt);
|
|
88
|
+
if (!customField) {
|
|
89
|
+
return 'Record<string, unknown>';
|
|
90
|
+
}
|
|
91
|
+
const customTypeName = emitCustomFieldType(createdAt, context);
|
|
92
|
+
return `({ fieldId: ${JSON.stringify(customField.fieldId)} } & ${customTypeName})`;
|
|
93
|
+
});
|
|
94
|
+
return `Array<${itemTypes.join(' | ')}>`;
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function renderTypePropertyLine(field, context) {
|
|
101
|
+
const propertyName = toTypePropertyName(field.fieldId);
|
|
102
|
+
const optionalMark = field.required ? '' : '?';
|
|
103
|
+
const typeExpression = resolveFieldType(field, context);
|
|
104
|
+
return ` ${propertyName}${optionalMark}: ${typeExpression};`;
|
|
105
|
+
}
|
|
106
|
+
function renderEndpointType(target) {
|
|
107
|
+
const endpoint = target.endpoint;
|
|
108
|
+
const apiType = normalizeApiType(target.schema.apiType ?? target.apiType);
|
|
109
|
+
const endpointBaseTypeName = toPascalCase(endpoint);
|
|
110
|
+
const context = {
|
|
111
|
+
endpointBaseTypeName,
|
|
112
|
+
customFieldByCreatedAt: new Map(target.schema.customFields.map((field) => [field.createdAt, field])),
|
|
113
|
+
customTypeNameByCreatedAt: new Map(),
|
|
114
|
+
usedCustomTypeNames: new Set(),
|
|
115
|
+
emittedCustomTypeNames: new Set(),
|
|
116
|
+
processingCustomFieldIds: new Set(),
|
|
117
|
+
emittedCustomTypeBlocks: [],
|
|
118
|
+
};
|
|
119
|
+
const schemaTypeName = `${endpointBaseTypeName}Schema`;
|
|
120
|
+
const contentTypeName = `${endpointBaseTypeName}Content`;
|
|
121
|
+
const rootFieldLines = target.schema.apiFields.map((field) => renderTypePropertyLine(field, context));
|
|
122
|
+
const responseTypeBlock = apiType === 'LIST'
|
|
123
|
+
? `\nexport type ${endpointBaseTypeName}ListResponse = MicroCMSListResponse<${schemaTypeName}>;`
|
|
124
|
+
: '';
|
|
125
|
+
const customTypesBlock = context.emittedCustomTypeBlocks.length > 0
|
|
126
|
+
? `${context.emittedCustomTypeBlocks.join('\n\n')}\n\n`
|
|
127
|
+
: '';
|
|
128
|
+
const schemaTypeBlock = rootFieldLines.length === 0
|
|
129
|
+
? `export type ${schemaTypeName} = Record<string, never>;`
|
|
130
|
+
: [`export type ${schemaTypeName} = {`, ...rootFieldLines, '};'].join('\n');
|
|
131
|
+
const contentTypeBlock = apiType === 'OBJECT'
|
|
132
|
+
? `export type ${contentTypeName} = ${schemaTypeName} & MicroCMSObjectContent;`
|
|
133
|
+
: `export type ${contentTypeName} = ${schemaTypeName} & MicroCMSListContent;`;
|
|
134
|
+
return [
|
|
135
|
+
`// Endpoint schema from Management API: ${endpoint} (${apiType})`,
|
|
136
|
+
'',
|
|
137
|
+
customTypesBlock + schemaTypeBlock,
|
|
138
|
+
'',
|
|
139
|
+
contentTypeBlock + responseTypeBlock,
|
|
140
|
+
'',
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
export function renderDefinitionsFile(targets) {
|
|
144
|
+
const sortedTargets = [...targets].sort((a, b) => a.endpoint.localeCompare(b.endpoint));
|
|
145
|
+
const commonTypesBlock = [
|
|
146
|
+
'// Generated by microcms-cli gen-types.',
|
|
147
|
+
'// Source: microCMS Management API (/api/v1/apis, /api/v1/apis/{endpoint}).',
|
|
148
|
+
'// Note: This command does not fetch content from microCMS Content API.',
|
|
149
|
+
'// DO NOT EDIT.',
|
|
150
|
+
'',
|
|
151
|
+
'export interface MicroCMSContentId {',
|
|
152
|
+
' id: string;',
|
|
153
|
+
'}',
|
|
154
|
+
'',
|
|
155
|
+
'export interface MicroCMSDate {',
|
|
156
|
+
' createdAt: string;',
|
|
157
|
+
' updatedAt: string;',
|
|
158
|
+
' publishedAt?: string;',
|
|
159
|
+
' revisedAt?: string;',
|
|
160
|
+
'}',
|
|
161
|
+
'',
|
|
162
|
+
'export interface MicroCMSImage {',
|
|
163
|
+
' url: string;',
|
|
164
|
+
' width?: number;',
|
|
165
|
+
' height?: number;',
|
|
166
|
+
' alt?: string;',
|
|
167
|
+
'}',
|
|
168
|
+
'',
|
|
169
|
+
'export interface MicroCMSListResponse<T> {',
|
|
170
|
+
' contents: (T & MicroCMSListContent)[];',
|
|
171
|
+
' totalCount: number;',
|
|
172
|
+
' limit: number;',
|
|
173
|
+
' offset: number;',
|
|
174
|
+
'}',
|
|
175
|
+
'',
|
|
176
|
+
'export type MicroCMSListContent = MicroCMSContentId & MicroCMSDate;',
|
|
177
|
+
'export type MicroCMSObjectContent = MicroCMSDate;',
|
|
178
|
+
'',
|
|
179
|
+
].join('\n');
|
|
180
|
+
const endpointBlocks = sortedTargets.map((target) => renderEndpointType(target)).join('\n');
|
|
181
|
+
return [commonTypesBlock, endpointBlocks].join('\n');
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { genTypesCommand } from './commands/gen-types/index.js';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name('microcms-cli')
|
|
7
|
+
.description('CLI for microCMS')
|
|
8
|
+
.version('0.1.0');
|
|
9
|
+
program
|
|
10
|
+
.command('gen-types [endpointId]')
|
|
11
|
+
.description('Fetch API schema from microCMS Management API and generate TypeScript types')
|
|
12
|
+
.option('-o, --output <path>', 'Output path (default: ./types/microcms.d.ts)', './types/microcms.d.ts')
|
|
13
|
+
.option('--all', 'Generate types for all endpoints')
|
|
14
|
+
.option('--service-domain <domain>', 'Override MICROCMS_SERVICE_DOMAIN')
|
|
15
|
+
.option('--api-key <key>', 'Override MICROCMS_MANAGEMENT_API_KEY')
|
|
16
|
+
.action(async (endpointId, options) => {
|
|
17
|
+
await genTypesCommand(endpointId, options);
|
|
18
|
+
});
|
|
19
|
+
// Show help if no command is provided
|
|
20
|
+
if (process.argv.length === 2) {
|
|
21
|
+
program.outputHelp();
|
|
22
|
+
}
|
|
23
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wato787/microcms-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-friendly CLI tool for microCMS, built with Bun",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"microcms-cli": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/index.js', 0o755)\"",
|
|
18
|
+
"dev": "bun src/index.ts",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"typecheck": "tsgo --noEmit",
|
|
22
|
+
"compile": "bun build ./src/index.ts --compile --outfile microcms-cli",
|
|
23
|
+
"prepack": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"picocolors": "^1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"@types/node": "^24.10.13",
|
|
32
|
+
"@typescript/native-preview": "^7.0.0-dev.20260220.1",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"microcms",
|
|
40
|
+
"cli",
|
|
41
|
+
"bun",
|
|
42
|
+
"typescript",
|
|
43
|
+
"mcp"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|