create-oke 0.10.3 → 0.11.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/README.md +2 -2
- package/package.json +2 -2
- package/src/ai-setup/apply.ts +113 -17
- package/src/ai-setup/catalog.ts +3 -3
- package/src/ai-setup/from-pref.ts +1 -1
- package/src/ai-setup/prompts.ts +1 -1
- package/src/cli.test.ts +183 -142
- package/src/cli.ts +190 -32
- package/src/create-defaults.test.ts +21 -15
- package/src/create-defaults.ts +33 -13
- package/src/customize-flow.test.ts +30 -35
- package/src/customize-flow.ts +68 -229
- package/src/drivers-catalog.ts +42 -48
- package/src/local-okengine.test.ts +50 -41
- package/src/local-okengine.ts +28 -14
- package/src/locales.test.ts +128 -0
- package/src/locales.ts +321 -0
- package/src/scaffold.ts +277 -33
- package/src/templates.ts +2 -8
- package/src/transform.test.ts +96 -36
- package/src/transform.ts +285 -110
- package/templates/advanced/.env.example +23 -3
- package/templates/advanced/README.md +1 -1
- package/templates/advanced/drizzle.config.ts +10 -13
- package/templates/advanced/oke.config.ts +12 -57
- package/templates/advanced/package.json +3 -1
- package/templates/advanced/src/app.ts +3 -10
- package/templates/advanced/src/core.ts +58 -0
- package/templates/advanced/src/db/schema.decl.ts +1 -1
- package/templates/advanced/src/db/seed/index.ts +2 -2
- package/templates/advanced/src/flows/notes/index.ts +1 -13
- package/templates/advanced/src/locales/index.ts +6 -0
- package/templates/advanced/tests/advanced.test.ts +1 -1
- package/templates/standard/.env.example +23 -3
- package/templates/standard/README.md +1 -1
- package/templates/standard/drizzle.config.ts +10 -13
- package/templates/standard/oke.config.ts +10 -56
- package/templates/standard/package.json +3 -1
- package/templates/standard/src/app.ts +3 -10
- package/templates/standard/src/core.ts +55 -0
- package/templates/standard/src/db/schema.decl.ts +1 -1
- package/templates/standard/src/db/seed/index.ts +2 -2
- package/templates/standard/src/flows/notes/index.ts +1 -10
- package/templates/standard/src/locales/index.ts +6 -0
- package/templates/standard/tests/standard.test.ts +1 -1
- package/templates/advanced/src/core/channels.ts +0 -13
- package/templates/advanced/src/core/gates.ts +0 -12
- package/templates/advanced/src/core/index.ts +0 -12
- package/templates/advanced/src/core/store.ts +0 -8
- package/templates/advanced/src/core/vault.ts +0 -7
- package/templates/advanced/src/locales/ar.ts +0 -18
- package/templates/standard/src/core/channels.ts +0 -13
- package/templates/standard/src/core/gates.ts +0 -12
- package/templates/standard/src/core/index.ts +0 -12
- package/templates/standard/src/core/store.ts +0 -5
- package/templates/standard/src/core/vault.ts +0 -7
- package/templates/standard/src/locales/ar.ts +0 -18
package/src/drivers-catalog.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Never lists ids that fail loud at boot (signal postgres/nats,
|
|
5
5
|
* ai bedrock/vertex). Clock + journal `postgres` are real (SKIP LOCKED
|
|
6
6
|
* CronStore / durable-run journal).
|
|
7
|
+
*
|
|
8
|
+
* Docker-first: `dev`/`prod` share compose-backed protocols; `test` uses
|
|
9
|
+
* PGLite for SQL and memory drivers elsewhere. SQLite is not offered.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import type { CreateDefaults, CreateProfile, EnvDriverPins } from "./create-defaults.ts";
|
|
@@ -15,31 +18,21 @@ export type DriverChoice = {
|
|
|
15
18
|
readonly label: string;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
|
-
/**
|
|
19
|
-
export const
|
|
21
|
+
/** Template `dev`/`prod` pins (Docker Compose runtime). */
|
|
22
|
+
export const TEMPLATE_DEV = {
|
|
20
23
|
sql: "postgres",
|
|
21
24
|
kv: "redis",
|
|
22
25
|
files: "s3",
|
|
23
26
|
signal: "redis",
|
|
24
27
|
clock: "postgres",
|
|
25
|
-
|
|
28
|
+
/** Built-in encrypted-at-rest store — SQL-backed, so no extra container. */
|
|
29
|
+
vault: "vault",
|
|
26
30
|
email: "smtp",
|
|
27
31
|
} as const;
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
export const TEMPLATE_LOCAL = {
|
|
31
|
-
sql: "sqlite",
|
|
32
|
-
kv: "memory",
|
|
33
|
-
files: "fs",
|
|
34
|
-
signal: "memory",
|
|
35
|
-
clock: "memory",
|
|
36
|
-
vault: "env",
|
|
37
|
-
email: "console",
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
/** Test-column defaults (always applied). */
|
|
33
|
+
/** Test-column defaults (PGLite for SQL — not a customize choice). */
|
|
41
34
|
export const TEMPLATE_TEST = {
|
|
42
|
-
sql: "
|
|
35
|
+
sql: "pglite",
|
|
43
36
|
kv: "memory",
|
|
44
37
|
files: "memory",
|
|
45
38
|
signal: "memory",
|
|
@@ -48,11 +41,11 @@ export const TEMPLATE_TEST = {
|
|
|
48
41
|
email: "console",
|
|
49
42
|
} as const;
|
|
50
43
|
|
|
44
|
+
/** @deprecated Use {@link TEMPLATE_DEV}. */
|
|
45
|
+
export const TEMPLATE_DOCKER_PROD = TEMPLATE_DEV;
|
|
46
|
+
|
|
51
47
|
export const SQL_CHOICES: readonly DriverChoice[] = [
|
|
52
|
-
{ value: "sqlite", label: "sqlite" },
|
|
53
48
|
{ value: "postgres", label: "postgres" },
|
|
54
|
-
{ value: "libsql", label: "libsql" },
|
|
55
|
-
{ value: "pglite", label: "pglite" },
|
|
56
49
|
{ value: "memory", label: "memory" },
|
|
57
50
|
];
|
|
58
51
|
|
|
@@ -71,7 +64,6 @@ export const INDEX_CHOICES: readonly DriverChoice[] = [
|
|
|
71
64
|
{ value: "none", label: "none" },
|
|
72
65
|
{ value: "memory", label: "memory" },
|
|
73
66
|
{ value: "pgvector", label: "pgvector" },
|
|
74
|
-
{ value: "libsql", label: "libsql" },
|
|
75
67
|
{ value: "meilisearch", label: "meilisearch" },
|
|
76
68
|
];
|
|
77
69
|
|
|
@@ -93,11 +85,17 @@ export const JOURNAL_CHOICES: readonly DriverChoice[] = [
|
|
|
93
85
|
{ value: "postgres", label: "postgres" },
|
|
94
86
|
];
|
|
95
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Vault backends. `vault` is okengine's own encrypted-at-rest store: it lives
|
|
90
|
+
* in the app's Postgres, so it needs no extra container, and it is the only
|
|
91
|
+
* backend with `oke vault init` / `seal` / `audit`. `managed` reads from a
|
|
92
|
+
* provider secret store (e.g. AWS Secrets Manager).
|
|
93
|
+
*/
|
|
96
94
|
export const VAULT_CHOICES: readonly DriverChoice[] = [
|
|
97
|
-
{ value: "env", label: "env" },
|
|
98
|
-
{ value: "
|
|
99
|
-
{ value: "managed", label: "managed" },
|
|
100
|
-
{ value: "memory", label: "memory" },
|
|
95
|
+
{ value: "env", label: "env (dotenv layers only)" },
|
|
96
|
+
{ value: "vault", label: "vault (built-in, encrypted at rest — recommended)" },
|
|
97
|
+
{ value: "managed", label: "managed (KMS / provider secret store)" },
|
|
98
|
+
{ value: "memory", label: "memory (test)" },
|
|
101
99
|
];
|
|
102
100
|
|
|
103
101
|
/** Email driver ids for `drivers.channel.email` (SMS/WhatsApp/push are other mediums). */
|
|
@@ -140,31 +138,30 @@ export const AI_PROVIDERS = [
|
|
|
140
138
|
export type AiProviderId = (typeof AI_PROVIDERS)[number]["value"];
|
|
141
139
|
|
|
142
140
|
/**
|
|
143
|
-
* Build
|
|
141
|
+
* Build `{ dev, test, prod }` pins (prod mirrors dev).
|
|
144
142
|
*
|
|
145
|
-
* @param
|
|
146
|
-
* @param dockerProd - Template docker/prod pin for this facet
|
|
143
|
+
* @param dev - Dev (Compose) driver
|
|
147
144
|
* @param test - Test pin
|
|
145
|
+
* @param prod - Prod pin (defaults to `dev`)
|
|
148
146
|
*/
|
|
149
|
-
export function
|
|
150
|
-
return {
|
|
147
|
+
export function pinsEnv(dev: string, test: string, prod: string = dev): EnvDriverPins {
|
|
148
|
+
return { dev, test, prod };
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
/**
|
|
154
|
-
*
|
|
152
|
+
* Docker-ready pins — `dev`/`prod` share the chosen driver; `test` is separate.
|
|
155
153
|
*
|
|
156
|
-
* @param
|
|
157
|
-
* @param docker - Docker driver (also copied to prod)
|
|
154
|
+
* @param dev - Dev/prod driver
|
|
158
155
|
* @param test - Test pin
|
|
159
156
|
*/
|
|
160
|
-
export function pinsDockerReady(
|
|
161
|
-
return
|
|
157
|
+
export function pinsDockerReady(dev: string, test: string): EnvDriverPins {
|
|
158
|
+
return pinsEnv(dev, test, dev);
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
/**
|
|
165
162
|
* Default recommended create-defaults (matches template driver maps).
|
|
166
163
|
*
|
|
167
|
-
* @param profile -
|
|
164
|
+
* @param profile - Always docker-ready (kept for call-site compat)
|
|
168
165
|
* @param template - Starter id
|
|
169
166
|
*/
|
|
170
167
|
export function recommendedDefaults(
|
|
@@ -172,9 +169,9 @@ export function recommendedDefaults(
|
|
|
172
169
|
template: TemplateId = "standard",
|
|
173
170
|
): CreateDefaults {
|
|
174
171
|
const store = {
|
|
175
|
-
sql:
|
|
176
|
-
kv:
|
|
177
|
-
files:
|
|
172
|
+
sql: pinsDockerReady(TEMPLATE_DEV.sql, TEMPLATE_TEST.sql),
|
|
173
|
+
kv: pinsDockerReady(TEMPLATE_DEV.kv, TEMPLATE_TEST.kv),
|
|
174
|
+
files: pinsDockerReady(TEMPLATE_DEV.files, TEMPLATE_TEST.files),
|
|
178
175
|
index: null as EnvDriverPins | null,
|
|
179
176
|
};
|
|
180
177
|
return {
|
|
@@ -183,19 +180,17 @@ export function recommendedDefaults(
|
|
|
183
180
|
profile,
|
|
184
181
|
drivers: {
|
|
185
182
|
store,
|
|
186
|
-
signal:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
TEMPLATE_TEST.signal,
|
|
190
|
-
),
|
|
191
|
-
clock: pinsLocalOnly(TEMPLATE_LOCAL.clock, TEMPLATE_DOCKER_PROD.clock, TEMPLATE_TEST.clock),
|
|
192
|
-
vault: pinsLocalOnly(TEMPLATE_LOCAL.vault, TEMPLATE_DOCKER_PROD.vault, TEMPLATE_TEST.vault),
|
|
183
|
+
signal: pinsDockerReady(TEMPLATE_DEV.signal, TEMPLATE_TEST.signal),
|
|
184
|
+
clock: pinsDockerReady(TEMPLATE_DEV.clock, TEMPLATE_TEST.clock),
|
|
185
|
+
vault: pinsDockerReady(TEMPLATE_DEV.vault, TEMPLATE_TEST.vault),
|
|
193
186
|
channel: {
|
|
194
|
-
email:
|
|
187
|
+
email: pinsDockerReady(TEMPLATE_DEV.email, TEMPLATE_TEST.email),
|
|
195
188
|
},
|
|
196
189
|
ai: null,
|
|
197
190
|
},
|
|
198
191
|
ai: { enabled: false, provider: null, driver: null },
|
|
192
|
+
locales: [],
|
|
193
|
+
pgdog: false,
|
|
199
194
|
updatedAt: new Date().toISOString(),
|
|
200
195
|
};
|
|
201
196
|
}
|
|
@@ -212,7 +207,7 @@ export type CustomizeFacetId =
|
|
|
212
207
|
| "email";
|
|
213
208
|
|
|
214
209
|
/**
|
|
215
|
-
* Facets shown for a template (AI is asked after
|
|
210
|
+
* Facets shown for a template (AI is asked after the env pass).
|
|
216
211
|
*
|
|
217
212
|
* @param template - Starter id
|
|
218
213
|
*/
|
|
@@ -238,7 +233,6 @@ export const DEFAULT_IMAGES: Readonly<Record<string, string>> = {
|
|
|
238
233
|
"store.kv": "redis:8-alpine",
|
|
239
234
|
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
240
235
|
"channel.email": "axllent/mailpit:v1.22.3",
|
|
241
|
-
vault: "openbao/openbao:2.6.1",
|
|
242
236
|
"store.index": "getmeili/meilisearch:v1.37",
|
|
243
237
|
ai: LLAMA_CPP_IMAGE,
|
|
244
238
|
};
|
|
@@ -10,50 +10,59 @@ import { resolveLocalOkengineRoot } from "./templates.ts";
|
|
|
10
10
|
import { resolveOkengineDependency } from "./transform.ts";
|
|
11
11
|
|
|
12
12
|
describe("materializeLocalOkengineDependency", () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// Cold `bun install` into ~/.oke can exceed the default 5s under full-suite load.
|
|
14
|
+
test(
|
|
15
|
+
"stages publish-shaped package without workspaces or devDependencies",
|
|
16
|
+
() => {
|
|
17
|
+
const root = resolveLocalOkengineRoot();
|
|
18
|
+
expect(root).not.toBeNull();
|
|
19
|
+
if (!root) return;
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const dep = materializeLocalOkengineDependency(root);
|
|
22
|
+
const stage = localOkengineStageDir();
|
|
23
|
+
expect(dep).toBe(`file:${stage}`);
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(stage, "package.json"), "utf8")) as {
|
|
26
|
+
name: string;
|
|
27
|
+
dependencies?: Record<string, string>;
|
|
28
|
+
devDependencies?: unknown;
|
|
29
|
+
workspaces?: unknown;
|
|
30
|
+
peerDependencies?: Record<string, string>;
|
|
31
|
+
exports?: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
expect(pkg.name).toBe("okengine");
|
|
34
|
+
expect(pkg.devDependencies).toBeUndefined();
|
|
35
|
+
expect(pkg.workspaces).toBeUndefined();
|
|
36
|
+
expect(pkg.peerDependencies).toBeUndefined();
|
|
37
|
+
expect(pkg.dependencies?.["oxc-parser"]).toBeDefined();
|
|
38
|
+
// Peers folded into dependencies so the out-of-tree stage can resolve them.
|
|
39
|
+
expect(pkg.dependencies?.["drizzle-orm"]).toBe("1.0.0-rc.4");
|
|
40
|
+
expect(pkg.dependencies?.["drizzle-kit"]).toBe("1.0.0-rc.4");
|
|
41
|
+
expect(pkg.dependencies?.["zod"]).toBeDefined();
|
|
42
|
+
expect(pkg.exports?.["./config"]).toBe("./src/config/index.ts");
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Real copies — Bun file: install drops directory symlinks.
|
|
45
|
+
const src = join(stage, "src");
|
|
46
|
+
expect(existsSync(src)).toBe(true);
|
|
47
|
+
expect(lstatSync(src).isSymbolicLink()).toBe(false);
|
|
48
|
+
expect(existsSync(join(stage, "src/config/index.ts"))).toBe(true);
|
|
49
|
+
expect(existsSync(join(stage, "node_modules", "zod"))).toBe(true);
|
|
50
|
+
expect(existsSync(join(stage, "node_modules", "drizzle-kit", "cli.mjs"))).toBe(true);
|
|
51
|
+
},
|
|
52
|
+
{ timeout: 60_000 },
|
|
53
|
+
);
|
|
49
54
|
|
|
50
|
-
test(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
test(
|
|
56
|
+
"resolveOkengineDependency uses the staged file: path locally",
|
|
57
|
+
() => {
|
|
58
|
+
const root = resolveLocalOkengineRoot();
|
|
59
|
+
expect(root).not.toBeNull();
|
|
60
|
+
if (!root) return;
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const dep = resolveOkengineDependency(root);
|
|
63
|
+
expect(dep).toBe(`file:${localOkengineStageDir()}`);
|
|
64
|
+
expect(dep).not.toBe(`file:${root}`);
|
|
65
|
+
},
|
|
66
|
+
{ timeout: 60_000 },
|
|
67
|
+
);
|
|
59
68
|
});
|
package/src/local-okengine.ts
CHANGED
|
@@ -95,9 +95,19 @@ export function materializeLocalOkengineDependency(localOkengineRoot: string): s
|
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
const stage = localOkengineStageDir();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
const pkgBody = `${JSON.stringify(consumer, null, 2)}\n`;
|
|
99
|
+
const stagePkgPath = join(stage, "package.json");
|
|
100
|
+
const stageReady =
|
|
101
|
+
existsSync(join(stage, "node_modules", "zod")) &&
|
|
102
|
+
existsSync(join(stage, "node_modules", "drizzle-kit", "cli.mjs")) &&
|
|
103
|
+
existsSync(stagePkgPath) &&
|
|
104
|
+
readFileSync(stagePkgPath, "utf8") === pkgBody;
|
|
105
|
+
|
|
106
|
+
if (!stageReady) {
|
|
107
|
+
rmSync(stage, { recursive: true, force: true });
|
|
108
|
+
mkdirSync(stage, { recursive: true });
|
|
109
|
+
writeFileSync(stagePkgPath, pkgBody, "utf8");
|
|
110
|
+
}
|
|
101
111
|
|
|
102
112
|
const entries =
|
|
103
113
|
consumer.files && consumer.files.length > 0
|
|
@@ -109,20 +119,24 @@ export function materializeLocalOkengineDependency(localOkengineRoot: string): s
|
|
|
109
119
|
if (!existsSync(from)) continue;
|
|
110
120
|
const to = join(stage, rel);
|
|
111
121
|
mkdirSync(dirname(to), { recursive: true });
|
|
122
|
+
// Refresh publish files every call so scaffold always sees current source.
|
|
123
|
+
rmSync(to, { recursive: true, force: true });
|
|
112
124
|
cpSync(from, to, { recursive: true, dereference: true });
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
if (!stageReady) {
|
|
128
|
+
const install = Bun.spawnSync({
|
|
129
|
+
cmd: ["bun", "install"],
|
|
130
|
+
cwd: stage,
|
|
131
|
+
stdout: "pipe",
|
|
132
|
+
stderr: "pipe",
|
|
133
|
+
});
|
|
134
|
+
if (install.exitCode !== 0) {
|
|
135
|
+
const err = new TextDecoder().decode(install.stderr).trim();
|
|
136
|
+
throw new Error(
|
|
137
|
+
`create-oke: bun install failed in local okengine stage${err ? `\n${err}` : ""}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
return `file:${stage}`;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale parsing + scaffold apply helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
applyLocalesToProject,
|
|
11
|
+
formatI18nConfig,
|
|
12
|
+
formatLocalesIndex,
|
|
13
|
+
parseExtraLocales,
|
|
14
|
+
replaceI18nConfig,
|
|
15
|
+
stripLegacyAppLocaleImports,
|
|
16
|
+
} from "./locales.ts";
|
|
17
|
+
|
|
18
|
+
describe("parseExtraLocales", () => {
|
|
19
|
+
test("parses ar and ar,fr", () => {
|
|
20
|
+
expect(parseExtraLocales("ar")).toEqual(["ar"]);
|
|
21
|
+
expect(parseExtraLocales("ar,fr")).toEqual(["ar", "fr"]);
|
|
22
|
+
expect(parseExtraLocales(" AR ; fr ")).toEqual(["ar", "fr"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("drops en and duplicates", () => {
|
|
26
|
+
expect(parseExtraLocales("en,ar,en,ar")).toEqual(["ar"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("rejects empty / invalid", () => {
|
|
30
|
+
expect(parseExtraLocales("")).toBeNull();
|
|
31
|
+
expect(parseExtraLocales("1ar")).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("formatI18nConfig", () => {
|
|
36
|
+
test("English-only omits dir", () => {
|
|
37
|
+
expect(formatI18nConfig(["en"])).toBe('i18n: { locales: ["en"], default: "en" }');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("Arabic adds rtl dir", () => {
|
|
41
|
+
expect(formatI18nConfig(["en", "ar"])).toBe(
|
|
42
|
+
'i18n: { locales: ["en", "ar"], default: "en", dir: { "ar": "rtl" } }',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("formatLocalesIndex", () => {
|
|
48
|
+
test("lists en then extras as relative imports", () => {
|
|
49
|
+
const src = formatLocalesIndex(["en", "ar", "fr"]);
|
|
50
|
+
expect(src).toContain('import "./en";');
|
|
51
|
+
expect(src).toContain('import "./ar";');
|
|
52
|
+
expect(src).toContain('import "./fr";');
|
|
53
|
+
expect(src.match(/import "\.\//g)?.length).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("stripLegacyAppLocaleImports", () => {
|
|
58
|
+
test("removes @/ and ./ per-locale imports", () => {
|
|
59
|
+
const src = `import "@/core";
|
|
60
|
+
import "@/locales/en";
|
|
61
|
+
import "./locales/ar";
|
|
62
|
+
|
|
63
|
+
import { oke } from "okengine/http";
|
|
64
|
+
`;
|
|
65
|
+
const next = stripLegacyAppLocaleImports(src);
|
|
66
|
+
expect(next).not.toContain('import "@/locales/');
|
|
67
|
+
expect(next).not.toContain('import "./locales/');
|
|
68
|
+
expect(next).toContain('import "@/core";');
|
|
69
|
+
expect(next).toContain('import { oke } from "okengine/http";');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("replaceI18nConfig", () => {
|
|
74
|
+
test("replaces nested dir block", () => {
|
|
75
|
+
const src = `export default defineConfig({
|
|
76
|
+
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
|
|
77
|
+
});
|
|
78
|
+
`;
|
|
79
|
+
const next = replaceI18nConfig(src, ["en"]);
|
|
80
|
+
expect(next).toContain('i18n: { locales: ["en"], default: "en" }');
|
|
81
|
+
expect(next).not.toContain("dir:");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("applyLocalesToProject", () => {
|
|
86
|
+
test("adds ar + fr files and updates locales index", () => {
|
|
87
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-locales-"));
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(join(dir, "src/locales"), { recursive: true });
|
|
90
|
+
mkdirSync(join(dir, "src"), { recursive: true });
|
|
91
|
+
writeFileSync(
|
|
92
|
+
join(dir, "oke.config.ts"),
|
|
93
|
+
`export default defineConfig({\n i18n: { locales: ["en"], default: "en" },\n});\n`,
|
|
94
|
+
"utf8",
|
|
95
|
+
);
|
|
96
|
+
writeFileSync(
|
|
97
|
+
join(dir, "src/app.ts"),
|
|
98
|
+
`import "@/core";\n\nexport const app = {};\n`,
|
|
99
|
+
"utf8",
|
|
100
|
+
);
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(dir, "src/core.ts"),
|
|
103
|
+
`export const noteCreatedMail = mail.template("note-created", {\n locales: ["en"],\n});\n`,
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
writeFileSync(join(dir, "src/locales/en.ts"), "export const en = {};\n", "utf8");
|
|
107
|
+
writeFileSync(join(dir, "src/locales/index.ts"), formatLocalesIndex(["en"]), "utf8");
|
|
108
|
+
|
|
109
|
+
applyLocalesToProject(dir, ["ar", "fr"]);
|
|
110
|
+
|
|
111
|
+
expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toContain('"ar"');
|
|
112
|
+
expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toContain('"fr"');
|
|
113
|
+
expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toContain('"ar": "rtl"');
|
|
114
|
+
const index = readFileSync(join(dir, "src/locales/index.ts"), "utf8");
|
|
115
|
+
expect(index).toContain('import "./en";');
|
|
116
|
+
expect(index).toContain('import "./ar";');
|
|
117
|
+
expect(index).toContain('import "./fr";');
|
|
118
|
+
expect(readFileSync(join(dir, "src/app.ts"), "utf8")).not.toContain('import "@/locales/');
|
|
119
|
+
expect(readFileSync(join(dir, "src/locales/ar.ts"), "utf8")).toContain("غير موجود");
|
|
120
|
+
expect(readFileSync(join(dir, "src/locales/fr.ts"), "utf8")).toContain("TODO: translate");
|
|
121
|
+
expect(readFileSync(join(dir, "src/core.ts"), "utf8")).toContain(
|
|
122
|
+
'locales: ["en", "ar", "fr"]',
|
|
123
|
+
);
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|