@tbsten/mir-core 0.0.1-alpha02
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/package.json +25 -0
- package/src/__tests__/errors.test.ts +71 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/i18n.test.ts +60 -0
- package/src/__tests__/remote-registry.test.ts +265 -0
- package/src/__tests__/safe-yaml-parser.test.ts +187 -0
- package/src/__tests__/snapshots/__snapshots__/error-messages.snapshot.test.ts.snap +27 -0
- package/src/__tests__/snapshots/__snapshots__/snippet-schema-output.snapshot.test.ts.snap +78 -0
- package/src/__tests__/snapshots/__snapshots__/template-output.snapshot.test.ts.snap +45 -0
- package/src/__tests__/snapshots/error-messages.snapshot.test.ts +76 -0
- package/src/__tests__/snapshots/snippet-schema-output.snapshot.test.ts +98 -0
- package/src/__tests__/snapshots/template-output.snapshot.test.ts +73 -0
- package/src/__tests__/snippet-schema.test.ts +180 -0
- package/src/__tests__/symlink-checker.test.ts +95 -0
- package/src/__tests__/template-engine.test.ts +137 -0
- package/src/__tests__/validate-name.test.ts +61 -0
- package/src/errors.ts +82 -0
- package/src/hooks.ts +63 -0
- package/src/i18n/index.ts +32 -0
- package/src/i18n/locales/en.ts +91 -0
- package/src/i18n/locales/ja.ts +91 -0
- package/src/i18n/types.ts +91 -0
- package/src/index.ts +84 -0
- package/src/lib/symlink-checker.ts +62 -0
- package/src/registry.ts +117 -0
- package/src/remote-registry.ts +260 -0
- package/src/safe-yaml-parser.ts +71 -0
- package/src/snippet-schema.ts +117 -0
- package/src/template-engine.ts +114 -0
- package/src/validate-name.ts +12 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mir-core: safe-yaml-parser のセキュリティテスト
|
|
3
|
+
*
|
|
4
|
+
* YAML Injection 攻撃シナリオ(チケット 008-yaml-injection)の検証
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
safeParseYaml,
|
|
9
|
+
checkNoRefInSchema,
|
|
10
|
+
YAML_MAX_SIZE_BYTES,
|
|
11
|
+
} from "../safe-yaml-parser.js";
|
|
12
|
+
import { ValidationError } from "../index.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// 正常系: 通常の YAML は引き続きパースできること
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
describe("safeParseYaml - 正常系", () => {
|
|
19
|
+
it("プレーンな文字列 YAML をパースできる", () => {
|
|
20
|
+
const result = safeParseYaml("name: my-snippet\n");
|
|
21
|
+
expect(result).toEqual({ name: "my-snippet" });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("ネストしたオブジェクトをパースできる", () => {
|
|
25
|
+
const yaml = `
|
|
26
|
+
name: react-hook
|
|
27
|
+
variables:
|
|
28
|
+
foo:
|
|
29
|
+
description: フック名
|
|
30
|
+
schema:
|
|
31
|
+
type: string
|
|
32
|
+
`;
|
|
33
|
+
const result = safeParseYaml(yaml) as Record<string, unknown>;
|
|
34
|
+
expect(result.name).toBe("react-hook");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("配列をパースできる", () => {
|
|
38
|
+
const yaml = "tags:\n - react\n - typescript\n";
|
|
39
|
+
const result = safeParseYaml(yaml) as Record<string, unknown>;
|
|
40
|
+
expect(result.tags).toEqual(["react", "typescript"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("真偽値をパースできる", () => {
|
|
44
|
+
const yaml = "enabled: true\ndisabled: false\n";
|
|
45
|
+
const result = safeParseYaml(yaml) as Record<string, unknown>;
|
|
46
|
+
expect(result.enabled).toBe(true);
|
|
47
|
+
expect(result.disabled).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("数値をパースできる", () => {
|
|
51
|
+
const yaml = "count: 42\nfloat: 3.14\n";
|
|
52
|
+
const result = safeParseYaml(yaml) as Record<string, unknown>;
|
|
53
|
+
expect(result.count).toBe(42);
|
|
54
|
+
expect(result.float).toBe(3.14);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("null をパースできる", () => {
|
|
58
|
+
const result = safeParseYaml("value: null\n") as Record<string, unknown>;
|
|
59
|
+
expect(result.value).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// YAML Bomb (Billion Laughs) 対策
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
describe("safeParseYaml - YAML Bomb 対策", () => {
|
|
68
|
+
it("最大サイズを超える YAML は ValidationError を投げる", () => {
|
|
69
|
+
const oversized = "x: " + "a".repeat(YAML_MAX_SIZE_BYTES);
|
|
70
|
+
expect(() => safeParseYaml(oversized)).toThrow(ValidationError);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("最大サイズちょうどの YAML は許可される (境界値)", () => {
|
|
74
|
+
// 境界値ちょうどのサイズのシンプルな YAML は OK
|
|
75
|
+
const small = "name: ok\n";
|
|
76
|
+
expect(() => safeParseYaml(small)).not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("エイリアスを大量に展開する YAML Bomb はサイズ制限で防がれる", () => {
|
|
80
|
+
// 実際の YAML bomb を小さめに再現
|
|
81
|
+
const bomb = [
|
|
82
|
+
"a: &a [lol,lol,lol,lol,lol,lol,lol,lol,lol]",
|
|
83
|
+
"b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]",
|
|
84
|
+
"c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]",
|
|
85
|
+
"d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]",
|
|
86
|
+
].join("\n");
|
|
87
|
+
// サイズは小さいのでパースは通るが、爆発的な展開はしない
|
|
88
|
+
// (js-yaml は alias を参照のまま扱い、展開しない)
|
|
89
|
+
// ここではエラーを投げないことを確認(サイズ制限内)
|
|
90
|
+
expect(() => safeParseYaml(bomb)).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// YAML タグ攻撃対策
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe("safeParseYaml - カスタムタグ攻撃対策", () => {
|
|
99
|
+
it("!!js/undefined タグは無視されて通常の文字列になる", () => {
|
|
100
|
+
// CORE_SCHEMA では未知のタグはエラーまたは文字列として扱われる
|
|
101
|
+
// js-yaml の CORE_SCHEMA は !!js タグを認識しないのでエラーになる
|
|
102
|
+
expect(() => safeParseYaml("name: !!js/undefined foo")).toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("!!js/regexp タグはエラーになる", () => {
|
|
106
|
+
expect(() => safeParseYaml("re: !!js/regexp /foo/")).toThrow();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("!!js/function タグはエラーになる", () => {
|
|
110
|
+
expect(() => safeParseYaml("fn: !!js/function 'function(){}'")).toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// checkNoRefInSchema - JSON Schema $ref 禁止チェック
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe("checkNoRefInSchema", () => {
|
|
119
|
+
it("$ref がなければエラーを投げない", () => {
|
|
120
|
+
const schema = { type: "string", default: "hello" };
|
|
121
|
+
expect(() => checkNoRefInSchema(schema)).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("$ref があれば ValidationError を投げる", () => {
|
|
125
|
+
const schema = { $ref: "https://evil.com/schema.json" };
|
|
126
|
+
expect(() => checkNoRefInSchema(schema)).toThrow(ValidationError);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("ネストされた $ref も検出する", () => {
|
|
130
|
+
const schema = {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
name: { $ref: "#/definitions/Name" },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
expect(() => checkNoRefInSchema(schema)).toThrow(ValidationError);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("$ref を含む文字列値は検出する(キーではなく値でも禁止)", () => {
|
|
140
|
+
// $ref というキーそのものを禁止
|
|
141
|
+
const schema = { anyOf: [{ $ref: "https://example.com/schema" }] };
|
|
142
|
+
expect(() => checkNoRefInSchema(schema)).toThrow(ValidationError);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("null や undefined を渡してもエラーにならない", () => {
|
|
146
|
+
expect(() => checkNoRefInSchema(null)).not.toThrow();
|
|
147
|
+
expect(() => checkNoRefInSchema(undefined)).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("空オブジェクトはエラーにならない", () => {
|
|
151
|
+
expect(() => checkNoRefInSchema({})).not.toThrow();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// parseSnippetYaml との統合確認
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
describe("parseSnippetYaml - $ref を含む snippet は拒否される", () => {
|
|
160
|
+
it("variables.schema に $ref があれば ValidationError を投げる", async () => {
|
|
161
|
+
const { parseSnippetYaml } = await import("../index.js");
|
|
162
|
+
const yaml = `
|
|
163
|
+
name: evil-snippet
|
|
164
|
+
variables:
|
|
165
|
+
name:
|
|
166
|
+
schema:
|
|
167
|
+
$ref: "https://evil.com/schema.json"
|
|
168
|
+
`;
|
|
169
|
+
expect(() => parseSnippetYaml(yaml)).toThrow(ValidationError);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("通常の snippet YAML は引き続きパースできる", async () => {
|
|
173
|
+
const { parseSnippetYaml } = await import("../index.js");
|
|
174
|
+
const yaml = `
|
|
175
|
+
name: normal-snippet
|
|
176
|
+
variables:
|
|
177
|
+
name:
|
|
178
|
+
description: 名前
|
|
179
|
+
schema:
|
|
180
|
+
type: string
|
|
181
|
+
default: world
|
|
182
|
+
`;
|
|
183
|
+
const def = parseSnippetYaml(yaml);
|
|
184
|
+
expect(def.name).toBe("normal-snippet");
|
|
185
|
+
expect(def.variables?.name?.schema?.type).toBe("string");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`エラーメッセージ snapshot (日本語) > FileConflictError 1`] = `"ファイル "index.ts" は既に存在します。\`--no-interactive\` で全て上書きするか、\`--out-dir\` で別ディレクトリを指定してください"`;
|
|
4
|
+
|
|
5
|
+
exports[`エラーメッセージ snapshot (日本語) > PathTraversalError 1`] = `"セキュリティエラー: ファイルパス "../etc/passwd" が出力範囲外を参照しています。テンプレートファイルを確認してください"`;
|
|
6
|
+
|
|
7
|
+
exports[`エラーメッセージ snapshot (日本語) > RegistryNotFoundError 1`] = `"Registry "official" が見つかりません。\`~/.mir/mirconfig.yaml\` で registry を設定してください"`;
|
|
8
|
+
|
|
9
|
+
exports[`エラーメッセージ snapshot (日本語) > RegistryRemoteError (名前あり) 1`] = `"Registry "official" はリモート registry のため publish できません"`;
|
|
10
|
+
|
|
11
|
+
exports[`エラーメッセージ snapshot (日本語) > RegistryRemoteError (名前なし) 1`] = `"リモート registry には publish できません"`;
|
|
12
|
+
|
|
13
|
+
exports[`エラーメッセージ snapshot (日本語) > SnippetAlreadyExistsError 1`] = `"Snippet "react-hook" は既に存在します。\`--force\` で上書きするか、別の名前を指定してください"`;
|
|
14
|
+
|
|
15
|
+
exports[`エラーメッセージ snapshot (日本語) > SnippetNotFoundError 1`] = `"Snippet "react-hook" が見つかりません。\`mir list\` で利用可能な snippet を確認するか、\`--registry\` で別の registry を指定してください"`;
|
|
16
|
+
|
|
17
|
+
exports[`エラーメッセージ snapshot (英語) > FileConflictError 1`] = `"File "index.ts" already exists. Use \`--no-interactive\` to overwrite all files or \`--out-dir\` to specify another directory"`;
|
|
18
|
+
|
|
19
|
+
exports[`エラーメッセージ snapshot (英語) > PathTraversalError 1`] = `"Security error: File path "../etc/passwd" references outside the output directory. Check your template files"`;
|
|
20
|
+
|
|
21
|
+
exports[`エラーメッセージ snapshot (英語) > RegistryNotFoundError 1`] = `"Registry "official" not found. Configure registries in \`~/.mir/mirconfig.yaml\`"`;
|
|
22
|
+
|
|
23
|
+
exports[`エラーメッセージ snapshot (英語) > RegistryRemoteError (名前あり) 1`] = `"Cannot publish to registry "official" because it is a remote registry"`;
|
|
24
|
+
|
|
25
|
+
exports[`エラーメッセージ snapshot (英語) > SnippetAlreadyExistsError 1`] = `"Snippet "react-hook" already exists. Use \`--force\` to overwrite or specify a different name"`;
|
|
26
|
+
|
|
27
|
+
exports[`エラーメッセージ snapshot (英語) > SnippetNotFoundError 1`] = `"Snippet "react-hook" not found. Use \`mir list\` to see available snippets or specify \`--registry\` for another registry"`;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`snippet スキーマ snapshot > hooks 付き snippet 定義の YAML 出力 1`] = `
|
|
4
|
+
"name: with-hooks
|
|
5
|
+
hooks:
|
|
6
|
+
before-install:
|
|
7
|
+
- echo: インストールを開始します
|
|
8
|
+
- input:
|
|
9
|
+
confirm:
|
|
10
|
+
description: 続行しますか?
|
|
11
|
+
answer-to: confirmed
|
|
12
|
+
schema:
|
|
13
|
+
type: boolean
|
|
14
|
+
default: true
|
|
15
|
+
after-install:
|
|
16
|
+
- echo: ✅ インストール完了
|
|
17
|
+
"
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`snippet スキーマ snapshot > suggests 付き変数の YAML 出力 1`] = `
|
|
21
|
+
"name: css-setup
|
|
22
|
+
variables:
|
|
23
|
+
framework:
|
|
24
|
+
description: CSSフレームワーク
|
|
25
|
+
suggests:
|
|
26
|
+
- tailwind
|
|
27
|
+
- vanilla-extract
|
|
28
|
+
- css-modules
|
|
29
|
+
schema:
|
|
30
|
+
type: string
|
|
31
|
+
default: tailwind
|
|
32
|
+
"
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
exports[`snippet スキーマ snapshot > フル構成 YAML のパース → シリアライズ往復 1`] = `
|
|
36
|
+
"name: full-example
|
|
37
|
+
description: フル構成のサンプル
|
|
38
|
+
variables:
|
|
39
|
+
name:
|
|
40
|
+
description: コンポーネント名
|
|
41
|
+
schema:
|
|
42
|
+
type: string
|
|
43
|
+
framework:
|
|
44
|
+
description: フレームワーク
|
|
45
|
+
suggests:
|
|
46
|
+
- react
|
|
47
|
+
- vue
|
|
48
|
+
schema:
|
|
49
|
+
type: string
|
|
50
|
+
default: react
|
|
51
|
+
hooks:
|
|
52
|
+
before-install:
|
|
53
|
+
- echo: セットアップ中...
|
|
54
|
+
after-install:
|
|
55
|
+
- echo: '完了: {{ name }}'
|
|
56
|
+
"
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
exports[`snippet スキーマ snapshot > 変数付き snippet 定義の YAML 出力 1`] = `
|
|
60
|
+
"name: react-hook
|
|
61
|
+
description: React カスタムフック雛形
|
|
62
|
+
variables:
|
|
63
|
+
name:
|
|
64
|
+
description: フック名
|
|
65
|
+
schema:
|
|
66
|
+
type: string
|
|
67
|
+
description:
|
|
68
|
+
description: 説明文
|
|
69
|
+
schema:
|
|
70
|
+
type: string
|
|
71
|
+
default: ''
|
|
72
|
+
"
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
exports[`snippet スキーマ snapshot > 最小限の snippet 定義の YAML 出力 1`] = `
|
|
76
|
+
"name: minimal
|
|
77
|
+
"
|
|
78
|
+
`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`テンプレート展開結果 snapshot > React コンポーネントテンプレート 1`] = `
|
|
4
|
+
"import React from "react";
|
|
5
|
+
|
|
6
|
+
export interface ButtonProps {
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Button({ children }: ButtonProps) {
|
|
11
|
+
return <div className="Button">{children}</div>;
|
|
12
|
+
}"
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
exports[`テンプレート展開結果 snapshot > React フックテンプレート 1`] = `
|
|
16
|
+
"import { useState, useCallback } from "react";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 認証フック
|
|
20
|
+
*/
|
|
21
|
+
export function useAuth() {
|
|
22
|
+
const [state, setState] = useState(null);
|
|
23
|
+
return { state, setState };
|
|
24
|
+
}"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`テンプレート展開結果 snapshot > ネストしたディレクトリパス展開 1`] = `"src/features/auth/index.ts"`;
|
|
28
|
+
|
|
29
|
+
exports[`テンプレート展開結果 snapshot > パス展開 1`] = `"hooks/useAuth.ts"`;
|
|
30
|
+
|
|
31
|
+
exports[`テンプレート展開結果 snapshot > 条件分岐付きテンプレート 1`] = `
|
|
32
|
+
"import type { FC } from "react";
|
|
33
|
+
|
|
34
|
+
export const App = () => {
|
|
35
|
+
return <div>App</div>;
|
|
36
|
+
};"
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
exports[`テンプレート展開結果 snapshot > 条件分岐付きテンプレート 2`] = `
|
|
40
|
+
"import React from "react";
|
|
41
|
+
|
|
42
|
+
export const App = () => {
|
|
43
|
+
return <div>App</div>;
|
|
44
|
+
};"
|
|
45
|
+
`;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mir-core: エラーメッセージの snapshot テスト (日英)
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
SnippetNotFoundError,
|
|
7
|
+
SnippetAlreadyExistsError,
|
|
8
|
+
RegistryNotFoundError,
|
|
9
|
+
RegistryRemoteError,
|
|
10
|
+
PathTraversalError,
|
|
11
|
+
FileConflictError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
setLocale,
|
|
14
|
+
} from "../../index.js";
|
|
15
|
+
|
|
16
|
+
// TODO: 現時点では理想の挙動をテストケースとして記述。後で有効化する。
|
|
17
|
+
|
|
18
|
+
describe("エラーメッセージ snapshot (日本語)", () => {
|
|
19
|
+
beforeEach(() => setLocale("ja"));
|
|
20
|
+
|
|
21
|
+
it("SnippetNotFoundError", () => {
|
|
22
|
+
expect(new SnippetNotFoundError("react-hook").message).toMatchSnapshot();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("SnippetAlreadyExistsError", () => {
|
|
26
|
+
expect(new SnippetAlreadyExistsError("react-hook").message).toMatchSnapshot();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("RegistryNotFoundError", () => {
|
|
30
|
+
expect(new RegistryNotFoundError("official").message).toMatchSnapshot();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("RegistryRemoteError (名前あり)", () => {
|
|
34
|
+
expect(new RegistryRemoteError("official").message).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("RegistryRemoteError (名前なし)", () => {
|
|
38
|
+
expect(new RegistryRemoteError().message).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("PathTraversalError", () => {
|
|
42
|
+
expect(new PathTraversalError("../etc/passwd").message).toMatchSnapshot();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("FileConflictError", () => {
|
|
46
|
+
expect(new FileConflictError("index.ts").message).toMatchSnapshot();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("エラーメッセージ snapshot (英語)", () => {
|
|
51
|
+
beforeEach(() => setLocale("en"));
|
|
52
|
+
|
|
53
|
+
it("SnippetNotFoundError", () => {
|
|
54
|
+
expect(new SnippetNotFoundError("react-hook").message).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("SnippetAlreadyExistsError", () => {
|
|
58
|
+
expect(new SnippetAlreadyExistsError("react-hook").message).toMatchSnapshot();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("RegistryNotFoundError", () => {
|
|
62
|
+
expect(new RegistryNotFoundError("official").message).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("RegistryRemoteError (名前あり)", () => {
|
|
66
|
+
expect(new RegistryRemoteError("official").message).toMatchSnapshot();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("PathTraversalError", () => {
|
|
70
|
+
expect(new PathTraversalError("../etc/passwd").message).toMatchSnapshot();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("FileConflictError", () => {
|
|
74
|
+
expect(new FileConflictError("index.ts").message).toMatchSnapshot();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mir-core: snippet スキーマ処理結果の snapshot テスト
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { parseSnippetYaml, serializeSnippetYaml } from "../../index.js";
|
|
6
|
+
|
|
7
|
+
// TODO: 現時点では理想の挙動をテストケースとして記述。後で有効化する。
|
|
8
|
+
|
|
9
|
+
describe("snippet スキーマ snapshot", () => {
|
|
10
|
+
it("最小限の snippet 定義の YAML 出力", () => {
|
|
11
|
+
const yaml = serializeSnippetYaml({ name: "minimal" });
|
|
12
|
+
expect(yaml).toMatchSnapshot();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("変数付き snippet 定義の YAML 出力", () => {
|
|
16
|
+
const yaml = serializeSnippetYaml({
|
|
17
|
+
name: "react-hook",
|
|
18
|
+
description: "React カスタムフック雛形",
|
|
19
|
+
variables: {
|
|
20
|
+
name: {
|
|
21
|
+
description: "フック名",
|
|
22
|
+
schema: { type: "string" },
|
|
23
|
+
},
|
|
24
|
+
description: {
|
|
25
|
+
description: "説明文",
|
|
26
|
+
schema: { type: "string", default: "" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
expect(yaml).toMatchSnapshot();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("hooks 付き snippet 定義の YAML 出力", () => {
|
|
34
|
+
const yaml = serializeSnippetYaml({
|
|
35
|
+
name: "with-hooks",
|
|
36
|
+
hooks: {
|
|
37
|
+
"before-install": [
|
|
38
|
+
{ echo: "インストールを開始します" },
|
|
39
|
+
{
|
|
40
|
+
input: {
|
|
41
|
+
confirm: {
|
|
42
|
+
description: "続行しますか?",
|
|
43
|
+
"answer-to": "confirmed",
|
|
44
|
+
schema: { type: "boolean", default: true },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
"after-install": [
|
|
50
|
+
{ echo: "✅ インストール完了" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
expect(yaml).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("suggests 付き変数の YAML 出力", () => {
|
|
58
|
+
const yaml = serializeSnippetYaml({
|
|
59
|
+
name: "css-setup",
|
|
60
|
+
variables: {
|
|
61
|
+
framework: {
|
|
62
|
+
description: "CSSフレームワーク",
|
|
63
|
+
suggests: ["tailwind", "vanilla-extract", "css-modules"],
|
|
64
|
+
schema: { type: "string", default: "tailwind" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
expect(yaml).toMatchSnapshot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("フル構成 YAML のパース → シリアライズ往復", () => {
|
|
72
|
+
const input = `
|
|
73
|
+
name: full-example
|
|
74
|
+
description: フル構成のサンプル
|
|
75
|
+
variables:
|
|
76
|
+
name:
|
|
77
|
+
description: コンポーネント名
|
|
78
|
+
schema:
|
|
79
|
+
type: string
|
|
80
|
+
framework:
|
|
81
|
+
description: フレームワーク
|
|
82
|
+
suggests:
|
|
83
|
+
- react
|
|
84
|
+
- vue
|
|
85
|
+
schema:
|
|
86
|
+
type: string
|
|
87
|
+
default: react
|
|
88
|
+
hooks:
|
|
89
|
+
before-install:
|
|
90
|
+
- echo: "セットアップ中..."
|
|
91
|
+
after-install:
|
|
92
|
+
- echo: "完了: {{ name }}"
|
|
93
|
+
`;
|
|
94
|
+
const parsed = parseSnippetYaml(input);
|
|
95
|
+
const serialized = serializeSnippetYaml(parsed);
|
|
96
|
+
expect(serialized).toMatchSnapshot();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mir-core: テンプレート展開結果の snapshot テスト
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { expandTemplate, expandPath } from "../../index.js";
|
|
6
|
+
|
|
7
|
+
// TODO: 現時点では理想の挙動をテストケースとして記述。後で有効化する。
|
|
8
|
+
|
|
9
|
+
describe("テンプレート展開結果 snapshot", () => {
|
|
10
|
+
it("React コンポーネントテンプレート", () => {
|
|
11
|
+
const template = `import React from "react";
|
|
12
|
+
|
|
13
|
+
export interface {{ name }}Props {
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function {{ name }}({ children }: {{ name }}Props) {
|
|
18
|
+
return <div className="{{ name }}">{children}</div>;
|
|
19
|
+
}`;
|
|
20
|
+
const result = expandTemplate(template, { name: "Button" });
|
|
21
|
+
expect(result).toMatchSnapshot();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("React フックテンプレート", () => {
|
|
25
|
+
const template = `import { useState, useCallback } from "react";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* {{ description }}
|
|
29
|
+
*/
|
|
30
|
+
export function {{ name }}() {
|
|
31
|
+
const [state, setState] = useState(null);
|
|
32
|
+
return { state, setState };
|
|
33
|
+
}`;
|
|
34
|
+
const result = expandTemplate(template, {
|
|
35
|
+
name: "useAuth",
|
|
36
|
+
description: "認証フック",
|
|
37
|
+
});
|
|
38
|
+
expect(result).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("条件分岐付きテンプレート", () => {
|
|
42
|
+
const template = `{{#if useTypescript}}
|
|
43
|
+
import type { FC } from "react";
|
|
44
|
+
{{else}}
|
|
45
|
+
import React from "react";
|
|
46
|
+
{{/if}}
|
|
47
|
+
|
|
48
|
+
export const {{ name }} = () => {
|
|
49
|
+
return <div>{{ name }}</div>;
|
|
50
|
+
};`;
|
|
51
|
+
expect(
|
|
52
|
+
expandTemplate(template, { name: "App", useTypescript: true }),
|
|
53
|
+
).toMatchSnapshot();
|
|
54
|
+
expect(
|
|
55
|
+
expandTemplate(template, { name: "App", useTypescript: false }),
|
|
56
|
+
).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("パス展開", () => {
|
|
60
|
+
expect(
|
|
61
|
+
expandPath("{{ dir }}/{{ name }}.ts", { dir: "hooks", name: "useAuth" }),
|
|
62
|
+
).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("ネストしたディレクトリパス展開", () => {
|
|
66
|
+
expect(
|
|
67
|
+
expandPath("src/{{ module }}/{{ name }}/index.ts", {
|
|
68
|
+
module: "features",
|
|
69
|
+
name: "auth",
|
|
70
|
+
}),
|
|
71
|
+
).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
});
|