@tbsten/mir-core 0.0.2-alpha02 → 0.0.2-alpha03

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.
Files changed (120) hide show
  1. package/dist/__tests__/errors.test.d.ts +1 -0
  2. package/dist/__tests__/errors.test.js +51 -0
  3. package/dist/__tests__/errors.test.js.map +1 -0
  4. package/dist/__tests__/helpers/string-helpers.test.d.ts +1 -0
  5. package/dist/__tests__/helpers/string-helpers.test.js +61 -0
  6. package/dist/__tests__/helpers/string-helpers.test.js.map +1 -0
  7. package/dist/__tests__/hooks.test.d.ts +1 -0
  8. package/dist/__tests__/hooks.test.js +125 -0
  9. package/dist/__tests__/hooks.test.js.map +1 -0
  10. package/dist/__tests__/i18n.test.d.ts +1 -0
  11. package/dist/__tests__/i18n.test.js +51 -0
  12. package/dist/__tests__/i18n.test.js.map +1 -0
  13. package/dist/__tests__/remote-registry.test.d.ts +1 -0
  14. package/dist/__tests__/remote-registry.test.js +194 -0
  15. package/dist/__tests__/remote-registry.test.js.map +1 -0
  16. package/dist/__tests__/safe-yaml-parser.test.d.ts +1 -0
  17. package/dist/__tests__/safe-yaml-parser.test.js +159 -0
  18. package/dist/__tests__/safe-yaml-parser.test.js.map +1 -0
  19. package/dist/__tests__/snapshots/error-messages.snapshot.test.d.ts +1 -0
  20. package/dist/__tests__/snapshots/error-messages.snapshot.test.js +52 -0
  21. package/dist/__tests__/snapshots/error-messages.snapshot.test.js.map +1 -0
  22. package/dist/__tests__/snapshots/snippet-schema-output.snapshot.test.d.ts +1 -0
  23. package/dist/__tests__/snapshots/snippet-schema-output.snapshot.test.js +93 -0
  24. package/dist/__tests__/snapshots/snippet-schema-output.snapshot.test.js.map +1 -0
  25. package/dist/__tests__/snapshots/template-output.snapshot.test.d.ts +1 -0
  26. package/dist/__tests__/snapshots/template-output.snapshot.test.js +60 -0
  27. package/dist/__tests__/snapshots/template-output.snapshot.test.js.map +1 -0
  28. package/dist/__tests__/snippet-schema.test.d.ts +1 -0
  29. package/dist/__tests__/snippet-schema.test.js +137 -0
  30. package/dist/__tests__/snippet-schema.test.js.map +1 -0
  31. package/dist/__tests__/symlink-checker.test.d.ts +1 -0
  32. package/dist/__tests__/symlink-checker.test.js +81 -0
  33. package/dist/__tests__/symlink-checker.test.js.map +1 -0
  34. package/dist/__tests__/template-engine.test.d.ts +1 -0
  35. package/dist/__tests__/template-engine.test.js +175 -0
  36. package/dist/__tests__/template-engine.test.js.map +1 -0
  37. package/dist/__tests__/validate-name.test.d.ts +1 -0
  38. package/dist/__tests__/validate-name.test.js +49 -0
  39. package/dist/__tests__/validate-name.test.js.map +1 -0
  40. package/dist/errors.d.ts +31 -0
  41. package/dist/errors.js +68 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/helpers/index.d.ts +5 -0
  44. package/dist/helpers/index.js +28 -0
  45. package/dist/helpers/index.js.map +1 -0
  46. package/dist/helpers/string-helpers.d.ts +15 -0
  47. package/dist/helpers/string-helpers.js +63 -0
  48. package/dist/helpers/string-helpers.js.map +1 -0
  49. package/dist/hooks.d.ts +9 -0
  50. package/dist/hooks.js +48 -0
  51. package/dist/hooks.js.map +1 -0
  52. package/dist/i18n/index.d.ts +6 -0
  53. package/dist/i18n/index.js +20 -0
  54. package/dist/i18n/index.js.map +1 -0
  55. package/dist/i18n/locales/en.d.ts +2 -0
  56. package/dist/i18n/locales/en.js +84 -0
  57. package/dist/i18n/locales/en.js.map +1 -0
  58. package/dist/i18n/locales/ja.d.ts +2 -0
  59. package/dist/i18n/locales/ja.js +84 -0
  60. package/dist/i18n/locales/ja.js.map +1 -0
  61. package/dist/i18n/types.d.ts +72 -0
  62. package/dist/i18n/types.js +2 -0
  63. package/dist/i18n/types.js.map +1 -0
  64. package/dist/index.d.ts +17 -0
  65. package/dist/index.js +23 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/lib/symlink-checker.d.ts +15 -0
  68. package/dist/lib/symlink-checker.js +54 -0
  69. package/dist/lib/symlink-checker.js.map +1 -0
  70. package/dist/registry.d.ts +8 -0
  71. package/dist/registry.js +86 -0
  72. package/dist/registry.js.map +1 -0
  73. package/dist/remote-registry.d.ts +51 -0
  74. package/dist/remote-registry.js +181 -0
  75. package/dist/remote-registry.js.map +1 -0
  76. package/dist/safe-yaml-parser.d.ts +26 -0
  77. package/{src/safe-yaml-parser.ts → dist/safe-yaml-parser.js} +28 -34
  78. package/dist/safe-yaml-parser.js.map +1 -0
  79. package/dist/snippet-schema.d.ts +38 -0
  80. package/dist/snippet-schema.js +63 -0
  81. package/dist/snippet-schema.js.map +1 -0
  82. package/dist/template-engine.d.ts +5 -0
  83. package/dist/template-engine.js +130 -0
  84. package/dist/template-engine.js.map +1 -0
  85. package/dist/validate-name.d.ts +1 -0
  86. package/dist/validate-name.js +9 -0
  87. package/dist/validate-name.js.map +1 -0
  88. package/package.json +6 -1
  89. package/src/__tests__/errors.test.ts +0 -71
  90. package/src/__tests__/helpers/string-helpers.test.ts +0 -100
  91. package/src/__tests__/hooks.test.ts +0 -145
  92. package/src/__tests__/i18n.test.ts +0 -60
  93. package/src/__tests__/remote-registry.test.ts +0 -265
  94. package/src/__tests__/safe-yaml-parser.test.ts +0 -187
  95. package/src/__tests__/snapshots/__snapshots__/error-messages.snapshot.test.ts.snap +0 -27
  96. package/src/__tests__/snapshots/__snapshots__/snippet-schema-output.snapshot.test.ts.snap +0 -78
  97. package/src/__tests__/snapshots/__snapshots__/template-output.snapshot.test.ts.snap +0 -45
  98. package/src/__tests__/snapshots/error-messages.snapshot.test.ts +0 -76
  99. package/src/__tests__/snapshots/snippet-schema-output.snapshot.test.ts +0 -98
  100. package/src/__tests__/snapshots/template-output.snapshot.test.ts +0 -73
  101. package/src/__tests__/snippet-schema.test.ts +0 -180
  102. package/src/__tests__/symlink-checker.test.ts +0 -95
  103. package/src/__tests__/template-engine.test.ts +0 -240
  104. package/src/__tests__/validate-name.test.ts +0 -61
  105. package/src/errors.ts +0 -82
  106. package/src/helpers/index.ts +0 -34
  107. package/src/helpers/string-helpers.ts +0 -76
  108. package/src/hooks.ts +0 -63
  109. package/src/i18n/index.ts +0 -32
  110. package/src/i18n/locales/en.ts +0 -96
  111. package/src/i18n/locales/ja.ts +0 -96
  112. package/src/i18n/types.ts +0 -96
  113. package/src/index.ts +0 -86
  114. package/src/lib/symlink-checker.ts +0 -62
  115. package/src/registry.ts +0 -120
  116. package/src/remote-registry.ts +0 -260
  117. package/src/snippet-schema.ts +0 -117
  118. package/src/template-engine.ts +0 -147
  119. package/src/validate-name.ts +0 -12
  120. package/tsconfig.json +0 -17
package/src/registry.ts DELETED
@@ -1,120 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parseSnippetYaml, type SnippetDefinition } from "./snippet-schema.js";
4
-
5
- export function listRegistrySnippets(registryPath: string): string[] {
6
- if (!fs.existsSync(registryPath)) {
7
- return [];
8
- }
9
- const entries = fs.readdirSync(registryPath);
10
- return entries
11
- .filter((e) => e.endsWith(".yaml"))
12
- .map((e) => e.replace(/\.yaml$/, ""));
13
- }
14
-
15
- export function snippetExistsInRegistry(
16
- registryPath: string,
17
- name: string,
18
- ): boolean {
19
- const yamlPath = path.join(registryPath, `${name}.yaml`);
20
- return fs.existsSync(yamlPath);
21
- }
22
-
23
- export function readSnippetFromRegistry(
24
- registryPath: string,
25
- name: string,
26
- ): SnippetDefinition {
27
- const yamlPath = path.join(registryPath, `${name}.yaml`);
28
- const content = fs.readFileSync(yamlPath, "utf-8");
29
- return parseSnippetYaml(content);
30
- }
31
-
32
- export function listTemplateFiles(
33
- registryPath: string,
34
- name: string,
35
- ): string[] {
36
- const dirPath = path.join(registryPath, name);
37
- if (!fs.existsSync(dirPath)) {
38
- return [];
39
- }
40
- return listFilesRecursive(dirPath, "");
41
- }
42
-
43
- const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
44
-
45
- function listFilesRecursive(basePath: string, relativePath: string): string[] {
46
- const fullPath = relativePath
47
- ? path.join(basePath, relativePath)
48
- : basePath;
49
- const entries = fs.readdirSync(fullPath, { withFileTypes: true });
50
- const files: string[] = [];
51
-
52
- for (const entry of entries) {
53
- if (IGNORED_FILES.has(entry.name)) continue;
54
- const entryRelative = relativePath
55
- ? path.join(relativePath, entry.name)
56
- : entry.name;
57
- if (entry.isDirectory()) {
58
- files.push(...listFilesRecursive(basePath, entryRelative));
59
- } else {
60
- files.push(entryRelative);
61
- }
62
- }
63
-
64
- return files;
65
- }
66
-
67
- export function readTemplateFile(
68
- registryPath: string,
69
- name: string,
70
- filePath: string,
71
- ): string {
72
- const fullPath = path.join(registryPath, name, filePath);
73
- return fs.readFileSync(fullPath, "utf-8");
74
- }
75
-
76
- export function copySnippetToRegistry(
77
- sourceDir: string,
78
- sourceYamlPath: string,
79
- registryPath: string,
80
- name: string,
81
- ): void {
82
- fs.mkdirSync(registryPath, { recursive: true });
83
-
84
- const destYaml = path.join(registryPath, `${name}.yaml`);
85
- fs.copyFileSync(sourceYamlPath, destYaml);
86
-
87
- const destDir = path.join(registryPath, name);
88
- copyDirectoryRecursive(sourceDir, destDir);
89
- }
90
-
91
- function copyDirectoryRecursive(src: string, dest: string): void {
92
- fs.mkdirSync(dest, { recursive: true });
93
-
94
- const entries = fs.readdirSync(src, { withFileTypes: true });
95
- for (const entry of entries) {
96
- const srcPath = path.join(src, entry.name);
97
- const destPath = path.join(dest, entry.name);
98
-
99
- if (entry.isDirectory()) {
100
- copyDirectoryRecursive(srcPath, destPath);
101
- } else {
102
- fs.copyFileSync(srcPath, destPath);
103
- }
104
- }
105
- }
106
-
107
- export function removeSnippetFromRegistry(
108
- registryPath: string,
109
- name: string,
110
- ): void {
111
- const yamlPath = path.join(registryPath, `${name}.yaml`);
112
- if (fs.existsSync(yamlPath)) {
113
- fs.unlinkSync(yamlPath);
114
- }
115
-
116
- const dirPath = path.join(registryPath, name);
117
- if (fs.existsSync(dirPath)) {
118
- fs.rmSync(dirPath, { recursive: true, force: true });
119
- }
120
- }
@@ -1,260 +0,0 @@
1
- import { RemoteRegistryFetchError, InvalidManifestError, MirError } from "./errors.js";
2
- import { parseSnippetYaml } from "./snippet-schema.js";
3
- import { expandTemplate, expandPath } from "./template-engine.js";
4
- import { t } from "./i18n/index.js";
5
- import type { SnippetDefinition } from "./snippet-schema.js";
6
-
7
- /**
8
- * リモート registry のマニフェスト (index.json) の型
9
- */
10
- export interface RegistryManifest {
11
- snippets: Record<string, { files: string[] }>;
12
- }
13
-
14
- /**
15
- * fetch オプション (タイムアウト等)
16
- */
17
- export interface FetchOptions {
18
- /** タイムアウト時間(ミリ秒)。未指定の場合はタイムアウトなし */
19
- timeoutMs?: number;
20
- }
21
-
22
- /**
23
- * リモートから取得した snippet の情報
24
- */
25
- export interface RemoteSnippet {
26
- definition: SnippetDefinition;
27
- files: Map<string, string>;
28
- }
29
-
30
- /**
31
- * キャッシュエントリの型
32
- */
33
- interface CacheEntry<T> {
34
- data: T;
35
- timestamp: number;
36
- }
37
-
38
- const CACHE_TTL_MS = 60000; // 60秒
39
- const manifestCache = new Map<string, CacheEntry<RegistryManifest>>();
40
- const snippetListCache = new Map<string, CacheEntry<string[]>>();
41
-
42
- /**
43
- * キャッシュが有効か確認
44
- */
45
- function isCacheValid<T>(entry: CacheEntry<T>): boolean {
46
- return Date.now() - entry.timestamp < CACHE_TTL_MS;
47
- }
48
-
49
- /**
50
- * すべてのリモート registry キャッシュをクリア
51
- */
52
- export function clearAllRemoteRegistryCaches(): void {
53
- manifestCache.clear();
54
- snippetListCache.clear();
55
- }
56
-
57
- function normalizeBaseUrl(baseUrl: string): string {
58
- return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
59
- }
60
-
61
- /**
62
- * AbortController を使ったタイムアウト付き fetch
63
- */
64
- async function fetchWithTimeout(
65
- url: string,
66
- options: FetchOptions = {},
67
- ): Promise<Response> {
68
- if (!options.timeoutMs) {
69
- return fetch(url);
70
- }
71
-
72
- const controller = new AbortController();
73
- const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
74
-
75
- try {
76
- return await fetch(url, { signal: controller.signal });
77
- } catch (err) {
78
- if (err instanceof Error && err.name === "AbortError") {
79
- const timeoutSec = Math.round(options.timeoutMs / 1000);
80
- throw new MirError(t("error.fetch-timeout", { url, timeout: timeoutSec }));
81
- }
82
- throw err;
83
- } finally {
84
- clearTimeout(timeoutId);
85
- }
86
- }
87
-
88
- /**
89
- * マニフェスト (index.json) を取得する(キャッシュ付き)
90
- */
91
- export async function fetchRegistryManifest(
92
- baseUrl: string,
93
- options: FetchOptions = {},
94
- ): Promise<RegistryManifest> {
95
- const normalizedUrl = normalizeBaseUrl(baseUrl);
96
- const cached = manifestCache.get(normalizedUrl);
97
-
98
- if (cached && isCacheValid(cached)) {
99
- return cached.data;
100
- }
101
-
102
- const url = `${normalizedUrl}/index.json`;
103
- let res: Response;
104
- try {
105
- res = await fetchWithTimeout(url, options);
106
- } catch (err) {
107
- if (err instanceof MirError) throw err;
108
- throw new RemoteRegistryFetchError(url);
109
- }
110
- if (!res.ok) {
111
- throw new RemoteRegistryFetchError(url, res.status);
112
- }
113
- let data: unknown;
114
- try {
115
- data = await res.json();
116
- } catch {
117
- throw new InvalidManifestError(url);
118
- }
119
- if (
120
- typeof data !== "object" ||
121
- data === null ||
122
- !("snippets" in data) ||
123
- typeof (data as RegistryManifest).snippets !== "object"
124
- ) {
125
- throw new InvalidManifestError(url);
126
- }
127
-
128
- const manifest = data as RegistryManifest;
129
- manifestCache.set(normalizedUrl, {
130
- data: manifest,
131
- timestamp: Date.now(),
132
- });
133
-
134
- return manifest;
135
- }
136
-
137
- /**
138
- * リモート registry の snippet 名一覧を返す(キャッシュ付き)
139
- */
140
- export async function listRemoteSnippets(
141
- baseUrl: string,
142
- options: FetchOptions = {},
143
- ): Promise<string[]> {
144
- const normalizedUrl = normalizeBaseUrl(baseUrl);
145
- const cached = snippetListCache.get(normalizedUrl);
146
-
147
- if (cached && isCacheValid(cached)) {
148
- return cached.data;
149
- }
150
-
151
- const manifest = await fetchRegistryManifest(baseUrl, options);
152
- const snippetList = Object.keys(manifest.snippets);
153
-
154
- snippetListCache.set(normalizedUrl, {
155
- data: snippetList,
156
- timestamp: Date.now(),
157
- });
158
-
159
- return snippetList;
160
- }
161
-
162
- /**
163
- * snippet 定義 (YAML) を取得する
164
- */
165
- export async function fetchSnippetDefinition(
166
- baseUrl: string,
167
- name: string,
168
- options: FetchOptions = {},
169
- ): Promise<SnippetDefinition> {
170
- const url = `${normalizeBaseUrl(baseUrl)}/${name}.yaml`;
171
- let res: Response;
172
- try {
173
- res = await fetchWithTimeout(url, options);
174
- } catch (err) {
175
- if (err instanceof MirError) throw err;
176
- throw new RemoteRegistryFetchError(url);
177
- }
178
- if (!res.ok) {
179
- throw new RemoteRegistryFetchError(url, res.status);
180
- }
181
- const text = await res.text();
182
- return parseSnippetYaml(text);
183
- }
184
-
185
- /**
186
- * テンプレートファイル群を並列で取得する
187
- */
188
- export async function fetchRemoteFiles(
189
- baseUrl: string,
190
- name: string,
191
- files: string[],
192
- options: FetchOptions = {},
193
- ): Promise<Map<string, string>> {
194
- const base = normalizeBaseUrl(baseUrl);
195
- const result = new Map<string, string>();
196
-
197
- const entries = await Promise.all(
198
- files.map(async (filePath) => {
199
- const url = `${base}/${name}/${encodeURIComponent(filePath)}`;
200
- let res: Response;
201
- try {
202
- res = await fetchWithTimeout(url, options);
203
- } catch (err) {
204
- if (err instanceof MirError) throw err;
205
- throw new RemoteRegistryFetchError(url);
206
- }
207
- if (!res.ok) {
208
- throw new RemoteRegistryFetchError(url, res.status);
209
- }
210
- const content = await res.text();
211
- return [filePath, content] as const;
212
- }),
213
- );
214
-
215
- for (const [filePath, content] of entries) {
216
- result.set(filePath, content);
217
- }
218
- return result;
219
- }
220
-
221
- /**
222
- * snippet 定義とテンプレートファイルを統合して取得する
223
- */
224
- export async function fetchRemoteSnippet(
225
- baseUrl: string,
226
- name: string,
227
- options: FetchOptions = {},
228
- ): Promise<RemoteSnippet> {
229
- const manifest = await fetchRegistryManifest(baseUrl, options);
230
- const snippetEntry = manifest.snippets[name];
231
- if (!snippetEntry) {
232
- throw new RemoteRegistryFetchError(
233
- `${normalizeBaseUrl(baseUrl)}/${name}.yaml`,
234
- 404,
235
- );
236
- }
237
-
238
- const [definition, files] = await Promise.all([
239
- fetchSnippetDefinition(baseUrl, name, options),
240
- fetchRemoteFiles(baseUrl, name, snippetEntry.files, options),
241
- ]);
242
-
243
- return { definition, files };
244
- }
245
-
246
- /**
247
- * リモートから取得したテンプレートファイルの変数を展開する
248
- */
249
- export function expandRemoteTemplateFiles(
250
- files: Map<string, string>,
251
- variables: Record<string, unknown>,
252
- ): Map<string, string> {
253
- const result = new Map<string, string>();
254
- for (const [filePath, content] of files) {
255
- const expandedPath = expandPath(filePath, variables);
256
- const expandedContent = expandTemplate(content, variables);
257
- result.set(expandedPath, expandedContent);
258
- }
259
- return result;
260
- }
@@ -1,117 +0,0 @@
1
- import yaml from "js-yaml";
2
- import { ValidationError } from "./errors.js";
3
- import { validateSnippetName } from "./validate-name.js";
4
- import { safeParseYaml, checkNoRefInSchema } from "./safe-yaml-parser.js";
5
-
6
- export interface VariableSchema {
7
- type?: "string" | "number" | "boolean";
8
- default?: unknown;
9
- enum?: unknown[];
10
- }
11
-
12
- export interface VariableDefinition {
13
- name?: string;
14
- description?: string;
15
- suggests?: string[];
16
- schema?: VariableSchema;
17
- }
18
-
19
- export interface Action {
20
- echo?: string;
21
- exit?: boolean;
22
- if?: string;
23
- input?: Record<
24
- string,
25
- {
26
- name?: string;
27
- description?: string;
28
- schema?: VariableSchema;
29
- "answer-to"?: string;
30
- }
31
- >;
32
- }
33
-
34
- export interface SnippetDefinition {
35
- name: string;
36
- /** semver 形式のバージョン文字列 (例: "1.0.0")。省略時は未バージョン管理扱い */
37
- version?: string;
38
- description?: string;
39
- tags?: string[];
40
- dependencies?: string[];
41
- variables?: Record<string, VariableDefinition>;
42
- hooks?: {
43
- "before-install"?: Action[];
44
- "after-install"?: Action[];
45
- };
46
- }
47
-
48
- export function parseSnippetYaml(content: string): SnippetDefinition {
49
- // 安全なパーサー使用(サイズ制限・カスタムタグ禁止)
50
- const parsed = safeParseYaml(content);
51
- if (typeof parsed !== "object" || parsed === null) {
52
- throw new ValidationError("snippet YAML のパースに失敗しました");
53
- }
54
- const def = parsed as SnippetDefinition;
55
- validateSnippetDefinition(def);
56
- return def;
57
- }
58
-
59
- export function serializeSnippetYaml(def: SnippetDefinition): string {
60
- return yaml.dump(def, { noRefs: true, lineWidth: -1 });
61
- }
62
-
63
- export function validateSnippetDefinition(def: SnippetDefinition): void {
64
- if (!def.name || typeof def.name !== "string") {
65
- throw new ValidationError("snippet 定義に name フィールドが必要です");
66
- }
67
- validateSnippetName(def.name);
68
- if (def.dependencies !== undefined) {
69
- if (!Array.isArray(def.dependencies)) {
70
- throw new ValidationError("dependencies は配列でなければなりません");
71
- }
72
- for (const dep of def.dependencies) {
73
- if (typeof dep !== "string") {
74
- throw new ValidationError(
75
- `dependencies の各要素は文字列でなければなりません。受け取った値: ${typeof dep}`,
76
- );
77
- }
78
- validateSnippetName(dep);
79
- }
80
- }
81
- if (def.variables !== undefined) {
82
- if (typeof def.variables !== "object" || def.variables === null) {
83
- throw new ValidationError("variables はオブジェクトでなければなりません");
84
- }
85
- for (const [key, varDef] of Object.entries(def.variables)) {
86
- if (typeof varDef !== "object" || varDef === null) {
87
- throw new ValidationError(
88
- `変数 "${key}" の定義はオブジェクトでなければなりません`,
89
- );
90
- }
91
- if (varDef.suggests !== undefined) {
92
- if (!Array.isArray(varDef.suggests)) {
93
- throw new ValidationError(
94
- `変数 "${key}" の suggests は配列でなければなりません`,
95
- );
96
- }
97
- for (const item of varDef.suggests) {
98
- if (typeof item !== "string") {
99
- throw new ValidationError(
100
- `変数 "${key}" の suggests の各要素は文字列でなければなりません`,
101
- );
102
- }
103
- }
104
- }
105
- // $ref によるJSON Schema外部参照攻撃を禁止
106
- checkNoRefInSchema(varDef.schema);
107
- if (varDef.schema?.type !== undefined) {
108
- const validTypes = ["string", "number", "boolean"];
109
- if (!validTypes.includes(varDef.schema.type)) {
110
- throw new ValidationError(
111
- `変数 "${key}" の type "${varDef.schema.type}" は無効です。string, number, boolean のいずれかを指定してください`,
112
- );
113
- }
114
- }
115
- }
116
- }
117
- }
@@ -1,147 +0,0 @@
1
- import Handlebars from "handlebars";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import {
5
- listTemplateFiles,
6
- readTemplateFile,
7
- } from "./registry.js";
8
- import { HELPER_NAMES, registerHelpers } from "./helpers/index.js";
9
-
10
- // 隔離インスタンスを作成しヘルパーを登録
11
- const hbs = Handlebars.create();
12
- registerHelpers(hbs);
13
-
14
- export function expandTemplate(
15
- template: string,
16
- variables: Record<string, unknown>,
17
- ): string {
18
- const compiled = hbs.compile(template, { noEscape: true });
19
- return compiled(variables);
20
- }
21
-
22
- export function expandPath(
23
- pathTemplate: string,
24
- variables: Record<string, unknown>,
25
- ): string {
26
- const expanded = expandTemplate(pathTemplate, variables);
27
- // 展開後のパスを正規化(空セグメント除去、区切り文字統一)
28
- return path.normalize(expanded);
29
- }
30
-
31
- /** Handlebars 組み込みヘルパー名 */
32
- const BUILTIN_HELPERS = new Set([
33
- "if",
34
- "unless",
35
- "each",
36
- "with",
37
- "lookup",
38
- "log",
39
- ]);
40
-
41
- export function extractVariables(template: string): string[] {
42
- const ast = Handlebars.parse(template);
43
- const vars = new Set<string>();
44
-
45
- function collectVarFromExpression(expr: hbs.AST.Expression): void {
46
- if (expr.type === "PathExpression") {
47
- const pathExpr = expr as hbs.AST.PathExpression;
48
- vars.add(pathExpr.parts[0]);
49
- } else if (expr.type === "SubExpression") {
50
- // サブ式: {{lowercase (replace name "/" ".")}} 等
51
- const sub = expr as hbs.AST.SubExpression;
52
- // sub.path はヘルパー名なのでスキップ、params のみ抽出
53
- if (sub.params) {
54
- for (const param of sub.params) collectVarFromExpression(param);
55
- }
56
- }
57
- }
58
-
59
- function visit(node: hbs.AST.Node): void {
60
- if (node.type === "MustacheStatement") {
61
- const stmt = node as hbs.AST.MustacheStatement;
62
- if (stmt.params && stmt.params.length > 0) {
63
- // ヘルパー呼び出し: path はヘルパー名なのでスキップ、params のみ変数抽出
64
- for (const param of stmt.params) collectVarFromExpression(param);
65
- } else {
66
- // 単純な変数参照: {{name}}
67
- if (stmt.path) collectVarFromExpression(stmt.path);
68
- }
69
- }
70
- if (node.type === "BlockStatement") {
71
- const block = node as hbs.AST.BlockStatement;
72
- // #if, #unless, #each 等のパラメータから変数を抽出
73
- if (block.params) {
74
- for (const param of block.params) collectVarFromExpression(param);
75
- }
76
- if (block.program) visit(block.program);
77
- if (block.inverse) visit(block.inverse);
78
- }
79
- if ("body" in node && Array.isArray((node as hbs.AST.Program).body)) {
80
- for (const child of (node as hbs.AST.Program).body) {
81
- visit(child);
82
- }
83
- }
84
- }
85
-
86
- visit(ast);
87
-
88
- // ヘルパー名・組み込みヘルパー名を除外
89
- for (const name of HELPER_NAMES) vars.delete(name);
90
- for (const name of BUILTIN_HELPERS) vars.delete(name);
91
-
92
- return [...vars];
93
- }
94
-
95
- const IGNORED_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
96
-
97
- export function extractVariablesFromDirectory(dirPath: string): string[] {
98
- const allVars = new Set<string>();
99
-
100
- function walkDir(currentPath: string): void {
101
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
102
- for (const entry of entries) {
103
- if (IGNORED_FILES.has(entry.name)) continue;
104
- const fullPath = path.join(currentPath, entry.name);
105
- if (entry.isDirectory()) {
106
- // ディレクトリ名からも変数を抽出
107
- for (const v of extractVariables(entry.name)) {
108
- allVars.add(v);
109
- }
110
- walkDir(fullPath);
111
- } else {
112
- // ファイル名から変数を抽出
113
- for (const v of extractVariables(entry.name)) {
114
- allVars.add(v);
115
- }
116
- // ファイル内容から変数を抽出
117
- const content = fs.readFileSync(fullPath, "utf-8");
118
- for (const v of extractVariables(content)) {
119
- allVars.add(v);
120
- }
121
- }
122
- }
123
- }
124
-
125
- if (fs.existsSync(dirPath)) {
126
- walkDir(dirPath);
127
- }
128
- return [...allVars];
129
- }
130
-
131
- export function expandTemplateDirectory(
132
- registryPath: string,
133
- snippetName: string,
134
- variables: Record<string, unknown>,
135
- ): Map<string, string> {
136
- const files = listTemplateFiles(registryPath, snippetName);
137
- const result = new Map<string, string>();
138
-
139
- for (const filePath of files) {
140
- const expandedPath = expandPath(filePath, variables);
141
- const content = readTemplateFile(registryPath, snippetName, filePath);
142
- const expandedContent = expandTemplate(content, variables);
143
- result.set(expandedPath, expandedContent);
144
- }
145
-
146
- return result;
147
- }
@@ -1,12 +0,0 @@
1
- import { ValidationError } from "./errors.js";
2
- import { t } from "./i18n/index.js";
3
-
4
- const SNIPPET_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
5
-
6
- export function validateSnippetName(name: string): void {
7
- if (!SNIPPET_NAME_PATTERN.test(name)) {
8
- throw new ValidationError(
9
- t("error.invalid-snippet-name", { name }),
10
- );
11
- }
12
- }
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "outDir": "dist",
10
- "rootDir": "src",
11
- "declaration": true,
12
- "sourceMap": true,
13
- "composite": true
14
- },
15
- "include": ["src"],
16
- "exclude": ["node_modules", "dist"]
17
- }