ink-cartridge 3.6.1
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 +249 -0
- package/dist/binary-storage/BinaryStorage.d.ts +308 -0
- package/dist/binary-storage/BinaryStorage.js +548 -0
- package/dist/binary-storage/StreamingReader.d.ts +53 -0
- package/dist/binary-storage/StreamingReader.js +530 -0
- package/dist/binary-storage/index.d.ts +91 -0
- package/dist/binary-storage/index.js +91 -0
- package/dist/binary-storage/types.d.ts +400 -0
- package/dist/binary-storage/types.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/initTheme.d.ts +14 -0
- package/dist/cli/initTheme.js +124 -0
- package/dist/cli/makeLanguageType.d.ts +46 -0
- package/dist/cli/makeLanguageType.js +264 -0
- package/dist/cli/makeThemeType.d.ts +43 -0
- package/dist/cli/makeThemeType.js +222 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/Badge.js +9 -0
- package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
- package/dist/components/dialog/ConfirmDialog.js +57 -0
- package/dist/components/dialog/types.d.ts +20 -0
- package/dist/components/dialog/types.js +1 -0
- package/dist/components/divider/Divider.d.ts +8 -0
- package/dist/components/divider/Divider.js +10 -0
- package/dist/components/fold/Fold.d.ts +3 -0
- package/dist/components/fold/Fold.js +44 -0
- package/dist/components/fold/types.d.ts +28 -0
- package/dist/components/fold/types.js +1 -0
- package/dist/components/form/Field.d.ts +30 -0
- package/dist/components/form/Field.js +59 -0
- package/dist/components/form/Form.d.ts +32 -0
- package/dist/components/form/Form.js +163 -0
- package/dist/components/form/context.d.ts +14 -0
- package/dist/components/form/context.js +20 -0
- package/dist/components/form/types.d.ts +115 -0
- package/dist/components/form/types.js +1 -0
- package/dist/components/key-hint/KeyHint.d.ts +10 -0
- package/dist/components/key-hint/KeyHint.js +12 -0
- package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
- package/dist/components/multi-select/MultiSelectInput.js +287 -0
- package/dist/components/multi-select/types.d.ts +100 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/number-input/NumberInput.d.ts +11 -0
- package/dist/components/number-input/NumberInput.js +44 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
- package/dist/components/progress-bar/ProgressBar.js +9 -0
- package/dist/components/search-input/SearchInput.d.ts +10 -0
- package/dist/components/search-input/SearchInput.js +15 -0
- package/dist/components/select/SelectInput.d.ts +100 -0
- package/dist/components/select/SelectInput.js +147 -0
- package/dist/components/select/types.d.ts +63 -0
- package/dist/components/select/types.js +1 -0
- package/dist/components/spinner/Spinner.d.ts +12 -0
- package/dist/components/spinner/Spinner.js +24 -0
- package/dist/components/tabs/Tabs.d.ts +3 -0
- package/dist/components/tabs/Tabs.js +57 -0
- package/dist/components/tabs/types.d.ts +32 -0
- package/dist/components/tabs/types.js +1 -0
- package/dist/components/text/TextInput.d.ts +39 -0
- package/dist/components/text/TextInput.js +233 -0
- package/dist/components/text/types.d.ts +62 -0
- package/dist/components/text/types.js +1 -0
- package/dist/dev/dev-screen.d.ts +36 -0
- package/dist/dev/dev-screen.js +142 -0
- package/dist/dev/entrance.d.ts +51 -0
- package/dist/dev/entrance.js +67 -0
- package/dist/dev/globalKey-display.d.ts +2 -0
- package/dist/dev/globalKey-display.js +8 -0
- package/dist/dev/types.d.ts +9 -0
- package/dist/dev/types.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/keyboard/check-global-key.d.ts +18 -0
- package/dist/keyboard/check-global-key.js +52 -0
- package/dist/keyboard/context.d.ts +398 -0
- package/dist/keyboard/context.js +9 -0
- package/dist/keyboard/global-key-processor/index.d.ts +16 -0
- package/dist/keyboard/global-key-processor/index.js +75 -0
- package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
- package/dist/keyboard/global-sequence-processor/index.js +165 -0
- package/dist/keyboard/hook.d.ts +51 -0
- package/dist/keyboard/hook.js +105 -0
- package/dist/keyboard/index.d.ts +5 -0
- package/dist/keyboard/index.js +3 -0
- package/dist/keyboard/keyNormalizer.d.ts +37 -0
- package/dist/keyboard/keyNormalizer.js +119 -0
- package/dist/keyboard/layer-handler.d.ts +57 -0
- package/dist/keyboard/layer-handler.js +274 -0
- package/dist/keyboard/modal-processor/index.d.ts +16 -0
- package/dist/keyboard/modal-processor/index.js +97 -0
- package/dist/keyboard/overlay-processor/index.d.ts +16 -0
- package/dist/keyboard/overlay-processor/index.js +30 -0
- package/dist/keyboard/pipeline/chain.d.ts +10 -0
- package/dist/keyboard/pipeline/chain.js +45 -0
- package/dist/keyboard/pipeline/context.d.ts +34 -0
- package/dist/keyboard/pipeline/context.js +42 -0
- package/dist/keyboard/pipeline/index.d.ts +3 -0
- package/dist/keyboard/pipeline/index.js +2 -0
- package/dist/keyboard/provider.d.ts +32 -0
- package/dist/keyboard/provider.js +985 -0
- package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
- package/dist/keyboard/screen-stack-processor/index.js +32 -0
- package/dist/keyboard/types.d.ts +632 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/language/context.d.ts +7 -0
- package/dist/language/context.js +7 -0
- package/dist/language/hook.d.ts +19 -0
- package/dist/language/hook.js +26 -0
- package/dist/language/index.d.ts +3 -0
- package/dist/language/index.js +2 -0
- package/dist/language/provider.d.ts +30 -0
- package/dist/language/provider.js +155 -0
- package/dist/language/types.d.ts +124 -0
- package/dist/language/types.js +1 -0
- package/dist/screen/ModalContext.d.ts +11 -0
- package/dist/screen/ModalContext.js +12 -0
- package/dist/screen/OverlayContext.d.ts +9 -0
- package/dist/screen/OverlayContext.js +10 -0
- package/dist/screen/context.d.ts +61 -0
- package/dist/screen/context.js +8 -0
- package/dist/screen/current-screen.d.ts +16 -0
- package/dist/screen/current-screen.js +38 -0
- package/dist/screen/hook.d.ts +13 -0
- package/dist/screen/hook.js +20 -0
- package/dist/screen/index.d.ts +7 -0
- package/dist/screen/index.js +5 -0
- package/dist/screen/provider.d.ts +112 -0
- package/dist/screen/provider.js +577 -0
- package/dist/screen/registry.d.ts +27 -0
- package/dist/screen/registry.js +71 -0
- package/dist/screen/types.d.ts +227 -0
- package/dist/screen/types.js +1 -0
- package/dist/storage/index.d.ts +45 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/storage.d.ts +139 -0
- package/dist/storage/storage.js +236 -0
- package/dist/storage/types.d.ts +145 -0
- package/dist/storage/types.js +1 -0
- package/dist/theme/context.d.ts +7 -0
- package/dist/theme/context.js +7 -0
- package/dist/theme/hook.d.ts +11 -0
- package/dist/theme/hook.js +18 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/provider.d.ts +18 -0
- package/dist/theme/provider.js +308 -0
- package/dist/theme/types.d.ts +53 -0
- package/dist/theme/types.js +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { makeLanguageType } from './makeLanguageType.js';
|
|
6
|
+
import { makeThemeType } from './makeThemeType.js';
|
|
7
|
+
import { initTheme } from './initTheme.js';
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const subcommand = args[0];
|
|
10
|
+
function printHelp() {
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(' ink-cartridge init <project-name> Scaffold a new project');
|
|
13
|
+
console.log(' ink-cartridge initTheme [--output <dir>] Interactive theme scaffold');
|
|
14
|
+
console.log(' ink-cartridge makeLanguageType <source> <out> Generate typed i18n bindings');
|
|
15
|
+
console.log(' ink-cartridge makeThemeType <source> <out> Generate typed theme bindings');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log('Options for makeLanguageType / makeThemeType:');
|
|
18
|
+
console.log(' --watch Re-generate on every file change');
|
|
19
|
+
console.log(' --debounce <ms> Debounce delay (default 500)');
|
|
20
|
+
console.log(' --from <pkg> Package name to import from (default ink-cartridge)');
|
|
21
|
+
console.log('');
|
|
22
|
+
}
|
|
23
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
24
|
+
printHelp();
|
|
25
|
+
process.exit(subcommand ? 0 : 1);
|
|
26
|
+
}
|
|
27
|
+
if (subcommand === 'init') {
|
|
28
|
+
const projectName = args[1];
|
|
29
|
+
if (!projectName) {
|
|
30
|
+
console.error('Error: project name is required');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const root = path.resolve(projectName);
|
|
34
|
+
if (fs.existsSync(root)) {
|
|
35
|
+
console.error(`Error: directory "${projectName}" already exists`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
console.log(`Creating ink-cartridge project: ${projectName}`);
|
|
39
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
40
|
+
const pkg = {
|
|
41
|
+
name: projectName,
|
|
42
|
+
version: '0.0.1',
|
|
43
|
+
private: true,
|
|
44
|
+
type: 'module',
|
|
45
|
+
scripts: {
|
|
46
|
+
start: 'tsx src/index.tsx',
|
|
47
|
+
dev: 'tsx watch src/index.tsx',
|
|
48
|
+
build: 'tsc',
|
|
49
|
+
test: 'echo "No tests yet"',
|
|
50
|
+
},
|
|
51
|
+
dependencies: {
|
|
52
|
+
'ink-cartridge': 'latest',
|
|
53
|
+
ink: '^7.0.1',
|
|
54
|
+
react: '^19.2.4',
|
|
55
|
+
},
|
|
56
|
+
devDependencies: {
|
|
57
|
+
'@types/node': '^20.19.39',
|
|
58
|
+
'@types/react': '^19.2.14',
|
|
59
|
+
tsx: '^4',
|
|
60
|
+
typescript: '^5.9.3',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
64
|
+
const tsconfig = {
|
|
65
|
+
compilerOptions: {
|
|
66
|
+
target: 'ES2022',
|
|
67
|
+
module: 'Node16',
|
|
68
|
+
lib: ['ES2022'],
|
|
69
|
+
outDir: './dist',
|
|
70
|
+
rootDir: './src',
|
|
71
|
+
strict: true,
|
|
72
|
+
moduleResolution: 'Node16',
|
|
73
|
+
esModuleInterop: true,
|
|
74
|
+
skipLibCheck: true,
|
|
75
|
+
jsx: 'react',
|
|
76
|
+
types: ['node'],
|
|
77
|
+
},
|
|
78
|
+
include: ['src/**/*'],
|
|
79
|
+
exclude: ['node_modules', 'dist'],
|
|
80
|
+
};
|
|
81
|
+
fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n');
|
|
82
|
+
const entry = `import React from 'react';
|
|
83
|
+
import { Box, Text, render } from 'ink';
|
|
84
|
+
import {
|
|
85
|
+
registerComponent,
|
|
86
|
+
ScenarioManagementProvider,
|
|
87
|
+
CurrentScreen,
|
|
88
|
+
useScreenSystem,
|
|
89
|
+
KeyboardProvider,
|
|
90
|
+
useKeyboard,
|
|
91
|
+
} from 'ink-cartridge';
|
|
92
|
+
|
|
93
|
+
function Menu() {
|
|
94
|
+
const { skip } = useScreenSystem();
|
|
95
|
+
const { boundKeyboard } = useKeyboard();
|
|
96
|
+
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
boundKeyboard(['s'], () => skip(Game));
|
|
99
|
+
boundKeyboard(['q'], () => process.exit(0));
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Box flexDirection="column" padding={1}>
|
|
104
|
+
<Text bold>ink-cartridge Demo</Text>
|
|
105
|
+
<Text>[S] Start [Q] Quit</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
registerComponent(Menu, {});
|
|
110
|
+
|
|
111
|
+
function Game() {
|
|
112
|
+
const { back } = useScreenSystem();
|
|
113
|
+
const { boundKeyboard } = useKeyboard();
|
|
114
|
+
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
boundKeyboard(['b'], () => back());
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Box flexDirection="column" padding={1}>
|
|
121
|
+
<Text bold>Game Screen</Text>
|
|
122
|
+
<Text>[B] Back to Menu</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
registerComponent(Game, {}, { parent: Menu });
|
|
127
|
+
|
|
128
|
+
function App() {
|
|
129
|
+
return (
|
|
130
|
+
<KeyboardProvider>
|
|
131
|
+
<CurrentScreen />
|
|
132
|
+
</KeyboardProvider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<ScenarioManagementProvider defaultScreen={Menu}>
|
|
138
|
+
<App />
|
|
139
|
+
</ScenarioManagementProvider>,
|
|
140
|
+
);
|
|
141
|
+
`;
|
|
142
|
+
fs.writeFileSync(path.join(root, 'src', 'index.tsx'), entry);
|
|
143
|
+
console.log('Installing dependencies...');
|
|
144
|
+
const result = spawnSync('npm', ['install'], { cwd: root, stdio: 'inherit' });
|
|
145
|
+
if (result.status !== 0) {
|
|
146
|
+
console.error('npm install failed. Run npm install manually.');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
console.log('\nDone! Run:');
|
|
150
|
+
console.log(` cd ${projectName}`);
|
|
151
|
+
console.log(' npm start');
|
|
152
|
+
}
|
|
153
|
+
/* ── initTheme ── */
|
|
154
|
+
if (subcommand === 'initTheme') {
|
|
155
|
+
const outputIndex = args.indexOf('--output');
|
|
156
|
+
const outputDir = outputIndex !== -1 ? args[outputIndex + 1] : './themes';
|
|
157
|
+
initTheme({ outputDir });
|
|
158
|
+
}
|
|
159
|
+
/* ── makeLanguageType ── */
|
|
160
|
+
if (subcommand === 'makeLanguageType') {
|
|
161
|
+
const sourceDir = args[1];
|
|
162
|
+
const outputDir = args[2];
|
|
163
|
+
if (!sourceDir || !outputDir) {
|
|
164
|
+
console.error('Error: ink-cartridge makeLanguageType <source-dir> <output-dir> [options]');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const watch = args.includes('--watch');
|
|
168
|
+
const debounceIndex = args.indexOf('--debounce');
|
|
169
|
+
const debounceMs = debounceIndex !== -1 ? parseInt(args[debounceIndex + 1], 10) : 500;
|
|
170
|
+
const fromIndex = args.indexOf('--from');
|
|
171
|
+
const packageName = fromIndex !== -1 ? args[fromIndex + 1] : 'ink-cartridge';
|
|
172
|
+
if (isNaN(debounceMs) || debounceMs < 0) {
|
|
173
|
+
console.error('Error: --debounce must be a non-negative number');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
makeLanguageType({
|
|
177
|
+
sourceDir: path.resolve(sourceDir),
|
|
178
|
+
outputDir: path.resolve(outputDir),
|
|
179
|
+
watch,
|
|
180
|
+
debounceMs,
|
|
181
|
+
packageName,
|
|
182
|
+
});
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
/* ── makeThemeType ── */
|
|
186
|
+
if (subcommand === 'makeThemeType') {
|
|
187
|
+
const sourceDir = args[1];
|
|
188
|
+
const outputDir = args[2];
|
|
189
|
+
if (!sourceDir || !outputDir) {
|
|
190
|
+
console.error('Error: ink-cartridge makeThemeType <source-dir> <output-dir> [options]');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const watch = args.includes('--watch');
|
|
194
|
+
const debounceIndex = args.indexOf('--debounce');
|
|
195
|
+
const debounceMs = debounceIndex !== -1 ? parseInt(args[debounceIndex + 1], 10) : 500;
|
|
196
|
+
const fromIndex = args.indexOf('--from');
|
|
197
|
+
const packageName = fromIndex !== -1 ? args[fromIndex + 1] : 'ink-cartridge';
|
|
198
|
+
if (isNaN(debounceMs) || debounceMs < 0) {
|
|
199
|
+
console.error('Error: --debounce must be a non-negative number');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
makeThemeType({
|
|
203
|
+
sourceDir: path.resolve(sourceDir),
|
|
204
|
+
outputDir: path.resolve(outputDir),
|
|
205
|
+
watch,
|
|
206
|
+
debounceMs,
|
|
207
|
+
packageName,
|
|
208
|
+
});
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
/* ── unknown subcommand ── */
|
|
212
|
+
console.error(`Error: unknown subcommand "${subcommand}"`);
|
|
213
|
+
printHelp();
|
|
214
|
+
process.exit(1);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface InitThemeOptions {
|
|
2
|
+
/** Output directory for theme JSON files (default: "./themes"). */
|
|
3
|
+
outputDir: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Interactive theme scaffold command.
|
|
7
|
+
*
|
|
8
|
+
* Prompts the user for theme ids, keys (color or style), and per-theme values,
|
|
9
|
+
* then writes one {id}.json file per theme with identical key sets.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* npx ink-cartridge initTheme --output ./my-themes
|
|
13
|
+
*/
|
|
14
|
+
export declare function initTheme(options: InitThemeOptions): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as readline from 'node:readline';
|
|
4
|
+
/**
|
|
5
|
+
* Interactive theme scaffold command.
|
|
6
|
+
*
|
|
7
|
+
* Prompts the user for theme ids, keys (color or style), and per-theme values,
|
|
8
|
+
* then writes one {id}.json file per theme with identical key sets.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* npx ink-cartridge initTheme --output ./my-themes
|
|
12
|
+
*/
|
|
13
|
+
export function initTheme(options) {
|
|
14
|
+
const rl = readline.createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
});
|
|
18
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
19
|
+
(async () => {
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(' ink-cartridge theme scaffold');
|
|
22
|
+
console.log(' ─────────────────────');
|
|
23
|
+
console.log('');
|
|
24
|
+
// Step 1: theme ids
|
|
25
|
+
const idsRaw = await ask(' Theme ids (comma-separated, e.g. "dark,light"): ');
|
|
26
|
+
const ids = idsRaw
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((s) => s.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
if (ids.length < 2) {
|
|
31
|
+
console.error('Error: at least 2 theme ids are required.');
|
|
32
|
+
rl.close();
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Validate no duplicates
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
if (seen.has(id)) {
|
|
39
|
+
console.error(`Error: duplicate theme id "${id}".`);
|
|
40
|
+
rl.close();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
seen.add(id);
|
|
44
|
+
}
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(` Themes: ${ids.join(', ')}`);
|
|
47
|
+
console.log('');
|
|
48
|
+
const keys = [];
|
|
49
|
+
console.log(' Define your theme keys one at a time.');
|
|
50
|
+
console.log(' Format: <key-name> <color|style>');
|
|
51
|
+
console.log(' Examples: primary color, titleBold style');
|
|
52
|
+
console.log(' Press Enter on an empty line when done.');
|
|
53
|
+
console.log('');
|
|
54
|
+
while (true) {
|
|
55
|
+
const line = await ask(' Key: ');
|
|
56
|
+
if (!line.trim())
|
|
57
|
+
break;
|
|
58
|
+
const parts = line.trim().split(/\s+/);
|
|
59
|
+
if (parts.length !== 2 || !['color', 'style'].includes(parts[1])) {
|
|
60
|
+
console.log(' Invalid format. Use: <key-name> <color|style>');
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const name = parts[0];
|
|
64
|
+
const kind = parts[1];
|
|
65
|
+
if (keys.some((k) => k.name === name)) {
|
|
66
|
+
console.log(` Key "${name}" already defined.`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
keys.push({ name, kind });
|
|
70
|
+
console.log(` ✓ Added ${kind} key: "${name}"`);
|
|
71
|
+
}
|
|
72
|
+
if (keys.length === 0) {
|
|
73
|
+
console.error('Error: at least one key is required.');
|
|
74
|
+
rl.close();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(` Keys (${keys.length}): ${keys.map((k) => `${k.name}(${k.kind})`).join(', ')}`);
|
|
79
|
+
console.log('');
|
|
80
|
+
// Step 3: per-theme values
|
|
81
|
+
const themes = {};
|
|
82
|
+
for (const id of ids) {
|
|
83
|
+
console.log(` ── Theme "${id}" ──`);
|
|
84
|
+
const values = { id };
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const defaultVal = key.kind === 'color' ? 'white' : 'false';
|
|
87
|
+
const val = await ask(` ${key.name} (${key.kind}, default: ${defaultVal}): `);
|
|
88
|
+
const trimmed = val.trim();
|
|
89
|
+
if (key.kind === 'color') {
|
|
90
|
+
values[key.name] = trimmed || 'white';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const lower = trimmed.toLowerCase();
|
|
94
|
+
values[key.name] = lower === 'true' || lower === '1' || lower === 'yes';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
themes[id] = values;
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
// Step 4: write files
|
|
101
|
+
const outDir = path.resolve(options.outputDir);
|
|
102
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
103
|
+
for (const id of ids) {
|
|
104
|
+
const filePath = path.join(outDir, `${id}.json`);
|
|
105
|
+
const obj = { id, ...themes[id] };
|
|
106
|
+
delete obj.id; // id is already there, we set it above — need to restructure
|
|
107
|
+
// Build clean object with id first
|
|
108
|
+
const clean = { id };
|
|
109
|
+
for (const [k, v] of Object.entries(themes[id])) {
|
|
110
|
+
if (k !== 'id')
|
|
111
|
+
clean[k] = v;
|
|
112
|
+
}
|
|
113
|
+
fs.writeFileSync(filePath, JSON.stringify(clean, null, 2) + '\n', 'utf-8');
|
|
114
|
+
console.log(` ✓ Written: ${filePath}`);
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(` Done! ${ids.length} themes with ${keys.length} keys → ${outDir}`);
|
|
118
|
+
rl.close();
|
|
119
|
+
})().catch((err) => {
|
|
120
|
+
console.error(`Error: ${err.message}`);
|
|
121
|
+
rl.close();
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
interface LanguageFile {
|
|
2
|
+
/** Locale code derived from the file name (e.g. "en-US"). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Flattened key → template string map. */
|
|
5
|
+
keys: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export interface MakeLanguageTypeOptions {
|
|
8
|
+
/** Directory containing {locale}.json files. */
|
|
9
|
+
sourceDir: string;
|
|
10
|
+
/** Directory where the generated files will be written. */
|
|
11
|
+
outputDir: string;
|
|
12
|
+
/** Whether to re-generate on file changes. */
|
|
13
|
+
watch: boolean;
|
|
14
|
+
/** Debounce delay in milliseconds (default 500). */
|
|
15
|
+
debounceMs: number;
|
|
16
|
+
/** npm package name to import the original t / useI18n from. */
|
|
17
|
+
packageName: string;
|
|
18
|
+
}
|
|
19
|
+
/** @internal Exported for testing only. */
|
|
20
|
+
export declare function flatJSON(obj: Record<string, unknown>, prefix?: string): Record<string, string>;
|
|
21
|
+
/** Keys present in EVERY language file. */
|
|
22
|
+
export declare function findCommonKeys(files: LanguageFile[]): string[];
|
|
23
|
+
/** Keys that exist in at least one file but not in all. */
|
|
24
|
+
export declare function findPartialKeys(files: LanguageFile[]): Map<string, string[]>;
|
|
25
|
+
/** Extract {param} names from a template string. */
|
|
26
|
+
export declare function extractParams(template: string): string[];
|
|
27
|
+
/** Build an array of template strings for a given key across all files. */
|
|
28
|
+
export declare function paramSetsForKey(key: string, files: LanguageFile[]): string[][];
|
|
29
|
+
/** @internal Exported for testing only. */
|
|
30
|
+
export declare function generateTypesContent(commonKeys: string[], keyParams: Record<string, string[]>): string;
|
|
31
|
+
/** @internal Exported for testing only. */
|
|
32
|
+
export declare function generateRuntimeContent(packageName: string): string;
|
|
33
|
+
/** @internal Exported for testing only. */
|
|
34
|
+
export declare function escapeSingleQuote(s: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Generate type-safe i18n bindings from a directory of {locale}.json files.
|
|
37
|
+
*
|
|
38
|
+
* Produces two files in {@link MakeLanguageTypeOptions.outputDir}:
|
|
39
|
+
* - `i18n-types.d.ts` — type declarations (TranslationKey, TranslationParams)
|
|
40
|
+
* - `i18n.ts` — typed `t()` function and `useI18n()` hook
|
|
41
|
+
*
|
|
42
|
+
* In watch mode it re-generates whenever a .json file in sourceDir changes,
|
|
43
|
+
* debounced to avoid rapid rebuilds.
|
|
44
|
+
*/
|
|
45
|
+
export declare function makeLanguageType(options: MakeLanguageTypeOptions): void;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* JSON flattening (must match src/language/provider.tsx) */
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/** @internal Exported for testing only. */
|
|
7
|
+
export function flatJSON(obj, prefix = '') {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
10
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
11
|
+
if (typeof value === 'string') {
|
|
12
|
+
result[fullKey] = value;
|
|
13
|
+
}
|
|
14
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
15
|
+
result[fullKey] = String(value);
|
|
16
|
+
}
|
|
17
|
+
else if (Array.isArray(value)) {
|
|
18
|
+
result[fullKey] = value.map((v) => String(v)).join(', ');
|
|
19
|
+
}
|
|
20
|
+
else if (typeof value === 'object' && value !== null) {
|
|
21
|
+
Object.assign(result, flatJSON(value, fullKey));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* File scanning */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
function scanLanguageFiles(dirPath) {
|
|
30
|
+
const files = [];
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = fs.readdirSync(dirPath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries.sort()) {
|
|
39
|
+
if (!entry.endsWith('.json'))
|
|
40
|
+
continue;
|
|
41
|
+
const name = entry.slice(0, -'.json'.length);
|
|
42
|
+
const fullPath = path.resolve(dirPath, entry);
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(fullPath, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
files.push({ name, keys: flatJSON(parsed) });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
process.stderr.write(`[ink-cartridge] Warning: failed to parse "${entry}" — skipped\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
/* Key analysis */
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
/** Keys present in EVERY language file. */
|
|
58
|
+
export function findCommonKeys(files) {
|
|
59
|
+
if (files.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const keySets = files.map((f) => new Set(Object.keys(f.keys)));
|
|
62
|
+
const common = new Set(keySets[0]);
|
|
63
|
+
for (let i = 1; i < keySets.length; i++) {
|
|
64
|
+
for (const key of common) {
|
|
65
|
+
if (!keySets[i].has(key))
|
|
66
|
+
common.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...common].sort();
|
|
70
|
+
}
|
|
71
|
+
/** Keys that exist in at least one file but not in all. */
|
|
72
|
+
export function findPartialKeys(files) {
|
|
73
|
+
if (files.length === 0)
|
|
74
|
+
return new Map();
|
|
75
|
+
const allKeys = new Set();
|
|
76
|
+
for (const f of files)
|
|
77
|
+
for (const k of Object.keys(f.keys))
|
|
78
|
+
allKeys.add(k);
|
|
79
|
+
const partial = new Map();
|
|
80
|
+
for (const key of allKeys) {
|
|
81
|
+
const missing = files.filter((f) => !(key in f.keys)).map((f) => f.name);
|
|
82
|
+
if (missing.length > 0)
|
|
83
|
+
partial.set(key, missing);
|
|
84
|
+
}
|
|
85
|
+
return partial;
|
|
86
|
+
}
|
|
87
|
+
/** Extract {param} names from a template string. */
|
|
88
|
+
export function extractParams(template) {
|
|
89
|
+
const params = new Set();
|
|
90
|
+
const re = /\{(\w+)\}/g;
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = re.exec(template)) !== null) {
|
|
93
|
+
params.add(match[1]);
|
|
94
|
+
}
|
|
95
|
+
return [...params].sort();
|
|
96
|
+
}
|
|
97
|
+
/** Build an array of template strings for a given key across all files. */
|
|
98
|
+
export function paramSetsForKey(key, files) {
|
|
99
|
+
return files.map((f) => {
|
|
100
|
+
const template = f.keys[key];
|
|
101
|
+
return template ? extractParams(template) : [];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/* ------------------------------------------------------------------ */
|
|
105
|
+
/* Code generation */
|
|
106
|
+
/* ------------------------------------------------------------------ */
|
|
107
|
+
/** @internal Exported for testing only. */
|
|
108
|
+
export function generateTypesContent(commonKeys, keyParams) {
|
|
109
|
+
const lines = [
|
|
110
|
+
'// Auto-generated by ink-cartridge makeLanguageType.',
|
|
111
|
+
'// Do not edit manually.',
|
|
112
|
+
'',
|
|
113
|
+
'export type TranslationKey =',
|
|
114
|
+
];
|
|
115
|
+
if (commonKeys.length === 0) {
|
|
116
|
+
lines.push(' never;');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
for (let i = 0; i < commonKeys.length; i++) {
|
|
120
|
+
const delim = i < commonKeys.length - 1 ? '' : ';';
|
|
121
|
+
lines.push(` | '${escapeSingleQuote(commonKeys[i])}'${delim}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
if (commonKeys.length === 0) {
|
|
126
|
+
lines.push('export type TranslationParams = Record<string, never>;');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
lines.push('export type TranslationParams = {');
|
|
130
|
+
for (const key of commonKeys) {
|
|
131
|
+
const params = keyParams[key];
|
|
132
|
+
if (!params || params.length === 0) {
|
|
133
|
+
lines.push(` '${escapeSingleQuote(key)}': undefined;`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const members = params.map((p) => ` ${p}: string | number;`).join('\n');
|
|
137
|
+
lines.push(` '${escapeSingleQuote(key)}': {\n${members}\n };`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
lines.push('};');
|
|
141
|
+
}
|
|
142
|
+
return lines.join('\n') + '\n';
|
|
143
|
+
}
|
|
144
|
+
/** @internal Exported for testing only. */
|
|
145
|
+
export function generateRuntimeContent(packageName) {
|
|
146
|
+
return (`// Auto-generated by ink-cartridge makeLanguageType.\n` +
|
|
147
|
+
`// Do not edit manually.\n` +
|
|
148
|
+
`\n` +
|
|
149
|
+
`import { t as rawT, useI18n as rawUseI18n } from '${escapeSingleQuote(packageName)}';\n` +
|
|
150
|
+
`import type { TranslationKey, TranslationParams } from './i18n-types.js';\n` +
|
|
151
|
+
`\n` +
|
|
152
|
+
`/**\n` +
|
|
153
|
+
` * Typed translation function. Drop-in replacement for the original \`t\`.\n` +
|
|
154
|
+
` *\n` +
|
|
155
|
+
` * @param key - A translation key shared by all language files.\n` +
|
|
156
|
+
` * @param options - Optional params and/or context.\n` +
|
|
157
|
+
` */\n` +
|
|
158
|
+
`export function t<K extends TranslationKey>(\n` +
|
|
159
|
+
` key: K,\n` +
|
|
160
|
+
` options?: { params?: TranslationParams[K]; context?: string },\n` +
|
|
161
|
+
`): string {\n` +
|
|
162
|
+
` return rawT(key, options as any);\n` +
|
|
163
|
+
`}\n` +
|
|
164
|
+
`\n` +
|
|
165
|
+
`/**\n` +
|
|
166
|
+
` * Typed \`useI18n\` hook. Returns the same API as the original hook but\n` +
|
|
167
|
+
` * with a type-safe \`t\` function.\n` +
|
|
168
|
+
` */\n` +
|
|
169
|
+
`export function useI18n(): Omit<ReturnType<typeof rawUseI18n>, 't'> & { t: typeof t } {\n` +
|
|
170
|
+
` const ctx = rawUseI18n();\n` +
|
|
171
|
+
` return { ...ctx, t: (key: any, options?: any) => ctx.t(key, options) };\n` +
|
|
172
|
+
`}\n`);
|
|
173
|
+
}
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
/* Helpers */
|
|
176
|
+
/* ------------------------------------------------------------------ */
|
|
177
|
+
/** @internal Exported for testing only. */
|
|
178
|
+
export function escapeSingleQuote(s) {
|
|
179
|
+
return s.replace(/'/g, "\\'");
|
|
180
|
+
}
|
|
181
|
+
function writeOutput(outputDir, typesContent, runtimeContent) {
|
|
182
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
183
|
+
fs.writeFileSync(path.join(outputDir, 'i18n-types.d.ts'), typesContent, 'utf-8');
|
|
184
|
+
fs.writeFileSync(path.join(outputDir, 'i18n.ts'), runtimeContent, 'utf-8');
|
|
185
|
+
}
|
|
186
|
+
/* ------------------------------------------------------------------ */
|
|
187
|
+
/* Public API */
|
|
188
|
+
/* ------------------------------------------------------------------ */
|
|
189
|
+
/**
|
|
190
|
+
* Generate type-safe i18n bindings from a directory of {locale}.json files.
|
|
191
|
+
*
|
|
192
|
+
* Produces two files in {@link MakeLanguageTypeOptions.outputDir}:
|
|
193
|
+
* - `i18n-types.d.ts` — type declarations (TranslationKey, TranslationParams)
|
|
194
|
+
* - `i18n.ts` — typed `t()` function and `useI18n()` hook
|
|
195
|
+
*
|
|
196
|
+
* In watch mode it re-generates whenever a .json file in sourceDir changes,
|
|
197
|
+
* debounced to avoid rapid rebuilds.
|
|
198
|
+
*/
|
|
199
|
+
export function makeLanguageType(options) {
|
|
200
|
+
const { sourceDir, outputDir, debounceMs } = options;
|
|
201
|
+
if (!fs.existsSync(sourceDir)) {
|
|
202
|
+
process.stderr.write(`[ink-cartridge] Error: source directory not found — "${sourceDir}"\n`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
generate(sourceDir, outputDir, options);
|
|
206
|
+
if (options.watch) {
|
|
207
|
+
let timer;
|
|
208
|
+
try {
|
|
209
|
+
fs.watch(sourceDir, (_eventType, filename) => {
|
|
210
|
+
if (!filename || !filename.endsWith('.json'))
|
|
211
|
+
return;
|
|
212
|
+
if (timer)
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
timer = setTimeout(() => {
|
|
215
|
+
process.stderr.write(`[ink-cartridge] Change detected in "${filename}", regenerating…\n`);
|
|
216
|
+
generate(sourceDir, outputDir, options);
|
|
217
|
+
}, debounceMs);
|
|
218
|
+
});
|
|
219
|
+
process.stderr.write(`[ink-cartridge] Watching "${sourceDir}" for changes (debounce: ${debounceMs}ms)…\n`);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
process.stderr.write(`[ink-cartridge] Error: watch mode failed — ${msg}\n`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/* ------------------------------------------------------------------ */
|
|
229
|
+
/* Core generate function */
|
|
230
|
+
/* ------------------------------------------------------------------ */
|
|
231
|
+
function generate(sourceDir, outputDir, options) {
|
|
232
|
+
const files = scanLanguageFiles(sourceDir);
|
|
233
|
+
if (files.length === 0) {
|
|
234
|
+
process.stderr.write(`[ink-cartridge] Warning: no .json files found in "${sourceDir}"\n`);
|
|
235
|
+
const typesContent = generateTypesContent([], {});
|
|
236
|
+
const runtimeContent = generateRuntimeContent(options.packageName);
|
|
237
|
+
writeOutput(outputDir, typesContent, runtimeContent);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Find partial keys first, emit warnings
|
|
241
|
+
const partialKeys = findPartialKeys(files);
|
|
242
|
+
if (partialKeys.size > 0) {
|
|
243
|
+
for (const [key, missingIn] of partialKeys) {
|
|
244
|
+
process.stderr.write(`[ink-cartridge] Warning: key "${key}" is missing in [${missingIn.join(', ')}] — excluded from type\n`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const commonKeys = findCommonKeys(files);
|
|
248
|
+
// Collect params for each common key (union across all files)
|
|
249
|
+
const keyParams = {};
|
|
250
|
+
for (const key of commonKeys) {
|
|
251
|
+
const allParams = new Set();
|
|
252
|
+
for (const paramsOfFile of paramSetsForKey(key, files)) {
|
|
253
|
+
for (const p of paramsOfFile)
|
|
254
|
+
allParams.add(p);
|
|
255
|
+
}
|
|
256
|
+
const sorted = [...allParams].sort();
|
|
257
|
+
if (sorted.length > 0)
|
|
258
|
+
keyParams[key] = sorted;
|
|
259
|
+
}
|
|
260
|
+
const typesContent = generateTypesContent(commonKeys, keyParams);
|
|
261
|
+
const runtimeContent = generateRuntimeContent(options.packageName);
|
|
262
|
+
writeOutput(outputDir, typesContent, runtimeContent);
|
|
263
|
+
process.stderr.write(`[ink-cartridge] Generated ${commonKeys.length} shared keys → ${path.resolve(outputDir)}\n`);
|
|
264
|
+
}
|