@warlock.js/core 4.2.2 → 4.2.5
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/CHANGELOG.md +24 -0
- package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +1 -0
- package/esm/dev-server/health-checker/workers/ts-health.worker.d.mts +1 -0
- package/esm/dev-server/loader/hook-thread.d.mts +48 -0
- package/esm/dev-server/loader/hook-thread.d.mts.map +1 -0
- package/esm/dev-server/loader/hook-thread.mjs +47 -0
- package/esm/dev-server/loader/hook-thread.mjs.map +1 -0
- package/esm/dev-server/loader/load-hook.d.mts +58 -0
- package/esm/dev-server/loader/load-hook.d.mts.map +1 -0
- package/esm/dev-server/loader/load-hook.mjs +86 -0
- package/esm/dev-server/loader/load-hook.mjs.map +1 -0
- package/esm/dev-server/loader/own-resolver.mjs +69 -0
- package/esm/dev-server/loader/own-resolver.mjs.map +1 -0
- package/esm/dev-server/loader/resolve-capture.mjs +43 -0
- package/esm/dev-server/loader/resolve-capture.mjs.map +1 -0
- package/esm/dev-server/loader/resolve-hook.d.mts +41 -0
- package/esm/dev-server/loader/resolve-hook.d.mts.map +1 -0
- package/esm/dev-server/loader/resolve-hook.mjs +87 -0
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -0
- package/esm/dev-server/loader/source-slug.mjs +22 -0
- package/esm/dev-server/loader/source-slug.mjs.map +1 -0
- package/esm/dev-server/loader/transpile-cache.mjs +163 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/loader/version-registry.mjs +45 -0
- package/esm/dev-server/loader/version-registry.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +10 -3
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +115 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/package.json +27 -9
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
|
|
4
5
|
//#region ../@warlock.js/core/src/dev-server/loader/transpile-cache.ts
|
|
5
6
|
/**
|
|
@@ -51,7 +52,168 @@ function computeFingerprint(parts) {
|
|
|
51
52
|
});
|
|
52
53
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Derive the cache key for a source file. The NUL separator makes
|
|
57
|
+
* `sourceText` and `fingerprint` unambiguous (no concatenation collision).
|
|
58
|
+
*/
|
|
59
|
+
function cacheKey(sourceText, fingerprint) {
|
|
60
|
+
return createHash("sha256").update(sourceText).update("\0").update(fingerprint).digest("hex");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Content-hash-addressed transpile cache stored on disk.
|
|
64
|
+
*
|
|
65
|
+
* Layout: `<cacheDir>/<first2-of-key>/<key>.js` plus a sibling `.js.map`.
|
|
66
|
+
* Sharding by the first two hex chars keeps any single directory small on
|
|
67
|
+
* large projects. GC metadata is derived from filesystem mtime so there is
|
|
68
|
+
* no second source of truth (no sidecar index to keep consistent).
|
|
69
|
+
*/
|
|
70
|
+
var TranspileCache = class {
|
|
71
|
+
constructor(cacheDir) {
|
|
72
|
+
this.cacheDir = cacheDir;
|
|
73
|
+
}
|
|
74
|
+
shardDir(key) {
|
|
75
|
+
return path.join(this.cacheDir, key.slice(0, 2));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `label` is an optional human-readable prefix (e.g. a path slug). It is
|
|
79
|
+
* cosmetic only — the content `key` is still what makes the name unique,
|
|
80
|
+
* so the same `(key, label)` pair must be passed to `get` and `put`.
|
|
81
|
+
*/
|
|
82
|
+
fileName(key, label, ext) {
|
|
83
|
+
return label ? `${label}.${key}${ext}` : `${key}${ext}`;
|
|
84
|
+
}
|
|
85
|
+
codePath(key, label) {
|
|
86
|
+
return path.join(this.shardDir(key), this.fileName(key, label, ".js"));
|
|
87
|
+
}
|
|
88
|
+
mapPath(key, label) {
|
|
89
|
+
return path.join(this.shardDir(key), this.fileName(key, label, ".js.map"));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Return the cached entry for `key`, or `null` on a miss. Synchronous on
|
|
93
|
+
* purpose: this sits on the module-load hot path and must not pay an
|
|
94
|
+
* event-loop hop per import.
|
|
95
|
+
*/
|
|
96
|
+
get(key, label) {
|
|
97
|
+
try {
|
|
98
|
+
const code = readFileSync(this.codePath(key, label), "utf8");
|
|
99
|
+
let map = "";
|
|
100
|
+
try {
|
|
101
|
+
map = readFileSync(this.mapPath(key, label), "utf8");
|
|
102
|
+
} catch {}
|
|
103
|
+
return {
|
|
104
|
+
code,
|
|
105
|
+
map
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Store an entry for `key`. Written atomically (temp file + rename) so a
|
|
113
|
+
* concurrent {@link get} never observes a half-written module.
|
|
114
|
+
*/
|
|
115
|
+
put(key, entry, label) {
|
|
116
|
+
mkdirSync(this.shardDir(key), { recursive: true });
|
|
117
|
+
this.atomicWrite(this.codePath(key, label), entry.code);
|
|
118
|
+
if (entry.map) this.atomicWrite(this.mapPath(key, label), entry.map);
|
|
119
|
+
}
|
|
120
|
+
atomicWrite(filePath, content) {
|
|
121
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
122
|
+
try {
|
|
123
|
+
writeFileSync(tempPath, content);
|
|
124
|
+
renameSync(tempPath, filePath);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
rmSync(tempPath, { force: true });
|
|
128
|
+
} catch {}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Opportunistic eviction. Cheap, runs at boot (not on the hot path).
|
|
134
|
+
* Removes age-expired entries first, then trims to the size budget by
|
|
135
|
+
* evicting least-recently-modified entries. Both bounds are optional;
|
|
136
|
+
* with neither set this is a no-op.
|
|
137
|
+
*/
|
|
138
|
+
gc(options = {}) {
|
|
139
|
+
const { maxBytes, maxAgeMs } = options;
|
|
140
|
+
if (maxBytes === void 0 && maxAgeMs === void 0) return;
|
|
141
|
+
const files = this.scan();
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
let live = files;
|
|
144
|
+
if (maxAgeMs !== void 0) {
|
|
145
|
+
live = [];
|
|
146
|
+
for (const file of files) if (now - file.mtimeMs > maxAgeMs) this.evict(file);
|
|
147
|
+
else live.push(file);
|
|
148
|
+
}
|
|
149
|
+
if (maxBytes !== void 0) {
|
|
150
|
+
let total = live.reduce((sum, file) => sum + file.size, 0);
|
|
151
|
+
if (total > maxBytes) {
|
|
152
|
+
live.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
153
|
+
for (const file of live) {
|
|
154
|
+
if (total <= maxBytes) break;
|
|
155
|
+
total -= file.size;
|
|
156
|
+
this.evict(file);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
scan() {
|
|
162
|
+
const result = [];
|
|
163
|
+
let shards;
|
|
164
|
+
try {
|
|
165
|
+
shards = readdirSync(this.cacheDir);
|
|
166
|
+
} catch {
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
for (const shard of shards) {
|
|
170
|
+
const shardPath = path.join(this.cacheDir, shard);
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = readdirSync(shardPath);
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (!entry.endsWith(".js")) continue;
|
|
179
|
+
const codePath = path.join(shardPath, entry);
|
|
180
|
+
try {
|
|
181
|
+
const stat = statSync(codePath);
|
|
182
|
+
const mapPath = `${codePath}.map`;
|
|
183
|
+
let mapSize = 0;
|
|
184
|
+
try {
|
|
185
|
+
mapSize = statSync(mapPath).size;
|
|
186
|
+
} catch {}
|
|
187
|
+
result.push({
|
|
188
|
+
codePath,
|
|
189
|
+
mapPath,
|
|
190
|
+
size: stat.size + mapSize,
|
|
191
|
+
mtimeMs: stat.mtimeMs
|
|
192
|
+
});
|
|
193
|
+
} catch {}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
evict(file) {
|
|
199
|
+
try {
|
|
200
|
+
rmSync(file.codePath, { force: true });
|
|
201
|
+
} catch {}
|
|
202
|
+
try {
|
|
203
|
+
rmSync(file.mapPath, { force: true });
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
/** Remove the entire cache directory (used by `--fresh`). */
|
|
207
|
+
clear() {
|
|
208
|
+
try {
|
|
209
|
+
rmSync(this.cacheDir, {
|
|
210
|
+
recursive: true,
|
|
211
|
+
force: true
|
|
212
|
+
});
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
54
216
|
|
|
55
217
|
//#endregion
|
|
56
|
-
export { computeFingerprint };
|
|
218
|
+
export { TranspileCache, cacheKey, computeFingerprint };
|
|
57
219
|
//# sourceMappingURL=transpile-cache.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transpile-cache.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/transpile-cache.ts"],"sourcesContent":["/**\n * Persisted transpile cache.\n *\n * Keyed by a hash of *source content* plus a transform-options fingerprint.\n * Same source + same options → same key → same output, always. Changed\n * content → different key → fresh transpile. There is no path-keyed entry\n * anywhere, so the old \"stack trace points at a mangled cache filename\" bug\n * class is structurally impossible — the on-disk filename is opaque and the\n * source identity lives only inside the source map.\n *\n * This module is pure and hook-agnostic on purpose: it knows how to read,\n * write and evict cache entries given a key, nothing about the ESM loader,\n * `?v=N` versioning, or esbuild. Phase 2 wires it into the load hook.\n *\n * @example\n * const fp = computeFingerprint({\n * esbuildVersion: esbuild.version,\n * cacheEpoch: CACHE_EPOCH,\n * compilerOptions,\n * });\n * const key = cacheKey(sourceText, fp);\n * const hit = cache.get(key);\n * if (!hit) cache.put(key, { code, map });\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * A cached transpile result: the emitted JS and its source map (raw JSON\n * string, exactly as esbuild produced it). Either may be empty for code\n * that produced no map.\n */\nexport type TranspileEntry = {\n code: string;\n map: string;\n};\n\n/**\n * Inputs that change transpiled output for identical source text. Bumping\n * any of these must invalidate every entry — that is achieved structurally\n * by folding them into the key, not by sweeping the cache.\n */\nexport type FingerprintParts = {\n /** `esbuild.version` — a transform-engine upgrade can change output. */\n esbuildVersion: string;\n /**\n * Monotonic cache-format epoch owned by the framework. Bump it whenever\n * the cache contract (entry shape, map handling, key derivation) changes\n * so old entries are guaranteed to miss after a framework upgrade.\n */\n cacheEpoch: number;\n /**\n * The resolved tsconfig `compilerOptions` blob, hashed wholesale rather\n * than cherry-picking fields — safer against tsconfig drift (a new option\n * that affects output can't silently serve stale code).\n */\n compilerOptions: unknown;\n};\n\n/**\n * Current cache-format epoch. Bump on any change to {@link TranspileEntry}\n * shape, the storage layout, or how the loader consumes entries.\n */\nexport const CACHE_EPOCH = 1;\n\n/**\n * Stable JSON stringify — object keys sorted recursively so semantically\n * equal option blobs always produce the same fingerprint regardless of key\n * insertion order.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map(key => {\n const child = stableStringify((value as Record<string, unknown>)[key]);\n return `${JSON.stringify(key)}:${child}`;\n });\n\n return `{${entries.join(\",\")}}`;\n}\n\n/**\n * Fold every output-affecting input into one short hex fingerprint that\n * becomes part of every cache key.\n */\nexport function computeFingerprint(parts: FingerprintParts): string {\n const canonical = stableStringify({\n esbuildVersion: parts.esbuildVersion,\n cacheEpoch: parts.cacheEpoch,\n compilerOptions: parts.compilerOptions,\n });\n\n return createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Derive the cache key for a source file. The NUL separator makes\n * `sourceText` and `fingerprint` unambiguous (no concatenation collision).\n */\nexport function cacheKey(sourceText: string, fingerprint: string): string {\n return createHash(\"sha256\")\n .update(sourceText)\n .update(\"\\0\")\n .update(fingerprint)\n .digest(\"hex\");\n}\n\n/** Options for {@link TranspileCache.gc}. */\nexport type GcOptions = {\n /** Evict least-recently-modified entries once total size exceeds this. */\n maxBytes?: number;\n /** Evict entries whose mtime is older than this many milliseconds. */\n maxAgeMs?: number;\n};\n\ntype CacheFileInfo = {\n codePath: string;\n mapPath: string;\n size: number;\n mtimeMs: number;\n};\n\n/**\n * Content-hash-addressed transpile cache stored on disk.\n *\n * Layout: `<cacheDir>/<first2-of-key>/<key>.js` plus a sibling `.js.map`.\n * Sharding by the first two hex chars keeps any single directory small on\n * large projects. GC metadata is derived from filesystem mtime so there is\n * no second source of truth (no sidecar index to keep consistent).\n */\nexport class TranspileCache {\n public constructor(private readonly cacheDir: string) {}\n\n private shardDir(key: string): string {\n return path.join(this.cacheDir, key.slice(0, 2));\n }\n\n /**\n * `label` is an optional human-readable prefix (e.g. a path slug). It is\n * cosmetic only — the content `key` is still what makes the name unique,\n * so the same `(key, label)` pair must be passed to `get` and `put`.\n */\n private fileName(key: string, label: string | undefined, ext: string): string {\n return label ? `${label}.${key}${ext}` : `${key}${ext}`;\n }\n\n private codePath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js\"));\n }\n\n private mapPath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js.map\"));\n }\n\n /**\n * Return the cached entry for `key`, or `null` on a miss. Synchronous on\n * purpose: this sits on the module-load hot path and must not pay an\n * event-loop hop per import.\n */\n public get(key: string, label?: string): TranspileEntry | null {\n try {\n const code = readFileSync(this.codePath(key, label), \"utf8\");\n let map = \"\";\n try {\n map = readFileSync(this.mapPath(key, label), \"utf8\");\n } catch {\n // A code entry with no map is valid (e.g. map-less transform).\n }\n return { code, map };\n } catch {\n return null;\n }\n }\n\n /**\n * Store an entry for `key`. Written atomically (temp file + rename) so a\n * concurrent {@link get} never observes a half-written module.\n */\n public put(key: string, entry: TranspileEntry, label?: string): void {\n const shard = this.shardDir(key);\n mkdirSync(shard, { recursive: true });\n\n this.atomicWrite(this.codePath(key, label), entry.code);\n if (entry.map) {\n this.atomicWrite(this.mapPath(key, label), entry.map);\n }\n }\n\n private atomicWrite(filePath: string, content: string): void {\n const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;\n try {\n writeFileSync(tempPath, content);\n renameSync(tempPath, filePath);\n } catch (error) {\n try {\n rmSync(tempPath, { force: true });\n } catch {\n // best effort — original error is the one that matters\n }\n throw error;\n }\n }\n\n /**\n * Opportunistic eviction. Cheap, runs at boot (not on the hot path).\n * Removes age-expired entries first, then trims to the size budget by\n * evicting least-recently-modified entries. Both bounds are optional;\n * with neither set this is a no-op.\n */\n public gc(options: GcOptions = {}): void {\n const { maxBytes, maxAgeMs } = options;\n if (maxBytes === undefined && maxAgeMs === undefined) return;\n\n const files = this.scan();\n const now = Date.now();\n\n let live = files;\n\n if (maxAgeMs !== undefined) {\n live = [];\n for (const file of files) {\n if (now - file.mtimeMs > maxAgeMs) {\n this.evict(file);\n } else {\n live.push(file);\n }\n }\n }\n\n if (maxBytes !== undefined) {\n let total = live.reduce((sum, file) => sum + file.size, 0);\n if (total > maxBytes) {\n // Oldest first — least-recently-modified is the eviction order.\n live.sort((a, b) => a.mtimeMs - b.mtimeMs);\n for (const file of live) {\n if (total <= maxBytes) break;\n total -= file.size;\n this.evict(file);\n }\n }\n }\n }\n\n private scan(): CacheFileInfo[] {\n const result: CacheFileInfo[] = [];\n\n let shards: string[];\n try {\n shards = readdirSync(this.cacheDir);\n } catch {\n return result;\n }\n\n for (const shard of shards) {\n const shardPath = path.join(this.cacheDir, shard);\n let entries: string[];\n try {\n entries = readdirSync(shardPath);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".js\")) continue;\n const codePath = path.join(shardPath, entry);\n try {\n const stat = statSync(codePath);\n const mapPath = `${codePath}.map`;\n let mapSize = 0;\n try {\n mapSize = statSync(mapPath).size;\n } catch {\n // no map sidecar — fine\n }\n result.push({\n codePath,\n mapPath,\n size: stat.size + mapSize,\n mtimeMs: stat.mtimeMs,\n });\n } catch {\n // entry vanished mid-scan — skip\n }\n }\n }\n\n return result;\n }\n\n private evict(file: CacheFileInfo): void {\n try {\n rmSync(file.codePath, { force: true });\n } catch {\n // best effort\n }\n try {\n rmSync(file.mapPath, { force: true });\n } catch {\n // best effort\n }\n }\n\n /** Remove the entire cache directory (used by `--fresh`). */\n public clear(): void {\n try {\n rmSync(this.cacheDir, { recursive: true, force: true });\n } catch {\n // best effort\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,UAAU,QAAQ,OAAO,UAAU,UACrC,OAAO,KAAK,UAAU,KAAK,KAAK;CAGlC,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,IAAI,MAAM,IAAI,eAAe,CAAC,CAAC,KAAK,GAAG,EAAE;CAUlD,OAAO,IAPS,OAAO,KAAK,KAAgC,CAAC,CAC1D,KAAK,CAAC,CACN,KAAI,QAAO;EACV,MAAM,QAAQ,gBAAiB,MAAkC,IAAI;EACrE,OAAO,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG;CACnC,CAEe,CAAC,CAAC,KAAK,GAAG,EAAE;AAC/B;;;;;AAMA,SAAgB,mBAAmB,OAAiC;CAClE,MAAM,YAAY,gBAAgB;EAChC,gBAAgB,MAAM;EACtB,YAAY,MAAM;EAClB,iBAAiB,MAAM;CACzB,CAAC;CAED,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;AACzE"}
|
|
1
|
+
{"version":3,"file":"transpile-cache.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/transpile-cache.ts"],"sourcesContent":["/**\n * Persisted transpile cache.\n *\n * Keyed by a hash of *source content* plus a transform-options fingerprint.\n * Same source + same options → same key → same output, always. Changed\n * content → different key → fresh transpile. There is no path-keyed entry\n * anywhere, so the old \"stack trace points at a mangled cache filename\" bug\n * class is structurally impossible — the on-disk filename is opaque and the\n * source identity lives only inside the source map.\n *\n * This module is pure and hook-agnostic on purpose: it knows how to read,\n * write and evict cache entries given a key, nothing about the ESM loader,\n * `?v=N` versioning, or esbuild. Phase 2 wires it into the load hook.\n *\n * @example\n * const fp = computeFingerprint({\n * esbuildVersion: esbuild.version,\n * cacheEpoch: CACHE_EPOCH,\n * compilerOptions,\n * });\n * const key = cacheKey(sourceText, fp);\n * const hit = cache.get(key);\n * if (!hit) cache.put(key, { code, map });\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * A cached transpile result: the emitted JS and its source map (raw JSON\n * string, exactly as esbuild produced it). Either may be empty for code\n * that produced no map.\n */\nexport type TranspileEntry = {\n code: string;\n map: string;\n};\n\n/**\n * Inputs that change transpiled output for identical source text. Bumping\n * any of these must invalidate every entry — that is achieved structurally\n * by folding them into the key, not by sweeping the cache.\n */\nexport type FingerprintParts = {\n /** `esbuild.version` — a transform-engine upgrade can change output. */\n esbuildVersion: string;\n /**\n * Monotonic cache-format epoch owned by the framework. Bump it whenever\n * the cache contract (entry shape, map handling, key derivation) changes\n * so old entries are guaranteed to miss after a framework upgrade.\n */\n cacheEpoch: number;\n /**\n * The resolved tsconfig `compilerOptions` blob, hashed wholesale rather\n * than cherry-picking fields — safer against tsconfig drift (a new option\n * that affects output can't silently serve stale code).\n */\n compilerOptions: unknown;\n};\n\n/**\n * Current cache-format epoch. Bump on any change to {@link TranspileEntry}\n * shape, the storage layout, or how the loader consumes entries.\n */\nexport const CACHE_EPOCH = 1;\n\n/**\n * Stable JSON stringify — object keys sorted recursively so semantically\n * equal option blobs always produce the same fingerprint regardless of key\n * insertion order.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n\n const entries = Object.keys(value as Record<string, unknown>)\n .sort()\n .map(key => {\n const child = stableStringify((value as Record<string, unknown>)[key]);\n return `${JSON.stringify(key)}:${child}`;\n });\n\n return `{${entries.join(\",\")}}`;\n}\n\n/**\n * Fold every output-affecting input into one short hex fingerprint that\n * becomes part of every cache key.\n */\nexport function computeFingerprint(parts: FingerprintParts): string {\n const canonical = stableStringify({\n esbuildVersion: parts.esbuildVersion,\n cacheEpoch: parts.cacheEpoch,\n compilerOptions: parts.compilerOptions,\n });\n\n return createHash(\"sha256\").update(canonical).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Derive the cache key for a source file. The NUL separator makes\n * `sourceText` and `fingerprint` unambiguous (no concatenation collision).\n */\nexport function cacheKey(sourceText: string, fingerprint: string): string {\n return createHash(\"sha256\")\n .update(sourceText)\n .update(\"\\0\")\n .update(fingerprint)\n .digest(\"hex\");\n}\n\n/** Options for {@link TranspileCache.gc}. */\nexport type GcOptions = {\n /** Evict least-recently-modified entries once total size exceeds this. */\n maxBytes?: number;\n /** Evict entries whose mtime is older than this many milliseconds. */\n maxAgeMs?: number;\n};\n\ntype CacheFileInfo = {\n codePath: string;\n mapPath: string;\n size: number;\n mtimeMs: number;\n};\n\n/**\n * Content-hash-addressed transpile cache stored on disk.\n *\n * Layout: `<cacheDir>/<first2-of-key>/<key>.js` plus a sibling `.js.map`.\n * Sharding by the first two hex chars keeps any single directory small on\n * large projects. GC metadata is derived from filesystem mtime so there is\n * no second source of truth (no sidecar index to keep consistent).\n */\nexport class TranspileCache {\n public constructor(private readonly cacheDir: string) {}\n\n private shardDir(key: string): string {\n return path.join(this.cacheDir, key.slice(0, 2));\n }\n\n /**\n * `label` is an optional human-readable prefix (e.g. a path slug). It is\n * cosmetic only — the content `key` is still what makes the name unique,\n * so the same `(key, label)` pair must be passed to `get` and `put`.\n */\n private fileName(key: string, label: string | undefined, ext: string): string {\n return label ? `${label}.${key}${ext}` : `${key}${ext}`;\n }\n\n private codePath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js\"));\n }\n\n private mapPath(key: string, label?: string): string {\n return path.join(this.shardDir(key), this.fileName(key, label, \".js.map\"));\n }\n\n /**\n * Return the cached entry for `key`, or `null` on a miss. Synchronous on\n * purpose: this sits on the module-load hot path and must not pay an\n * event-loop hop per import.\n */\n public get(key: string, label?: string): TranspileEntry | null {\n try {\n const code = readFileSync(this.codePath(key, label), \"utf8\");\n let map = \"\";\n try {\n map = readFileSync(this.mapPath(key, label), \"utf8\");\n } catch {\n // A code entry with no map is valid (e.g. map-less transform).\n }\n return { code, map };\n } catch {\n return null;\n }\n }\n\n /**\n * Store an entry for `key`. Written atomically (temp file + rename) so a\n * concurrent {@link get} never observes a half-written module.\n */\n public put(key: string, entry: TranspileEntry, label?: string): void {\n const shard = this.shardDir(key);\n mkdirSync(shard, { recursive: true });\n\n this.atomicWrite(this.codePath(key, label), entry.code);\n if (entry.map) {\n this.atomicWrite(this.mapPath(key, label), entry.map);\n }\n }\n\n private atomicWrite(filePath: string, content: string): void {\n const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;\n try {\n writeFileSync(tempPath, content);\n renameSync(tempPath, filePath);\n } catch (error) {\n try {\n rmSync(tempPath, { force: true });\n } catch {\n // best effort — original error is the one that matters\n }\n throw error;\n }\n }\n\n /**\n * Opportunistic eviction. Cheap, runs at boot (not on the hot path).\n * Removes age-expired entries first, then trims to the size budget by\n * evicting least-recently-modified entries. Both bounds are optional;\n * with neither set this is a no-op.\n */\n public gc(options: GcOptions = {}): void {\n const { maxBytes, maxAgeMs } = options;\n if (maxBytes === undefined && maxAgeMs === undefined) return;\n\n const files = this.scan();\n const now = Date.now();\n\n let live = files;\n\n if (maxAgeMs !== undefined) {\n live = [];\n for (const file of files) {\n if (now - file.mtimeMs > maxAgeMs) {\n this.evict(file);\n } else {\n live.push(file);\n }\n }\n }\n\n if (maxBytes !== undefined) {\n let total = live.reduce((sum, file) => sum + file.size, 0);\n if (total > maxBytes) {\n // Oldest first — least-recently-modified is the eviction order.\n live.sort((a, b) => a.mtimeMs - b.mtimeMs);\n for (const file of live) {\n if (total <= maxBytes) break;\n total -= file.size;\n this.evict(file);\n }\n }\n }\n }\n\n private scan(): CacheFileInfo[] {\n const result: CacheFileInfo[] = [];\n\n let shards: string[];\n try {\n shards = readdirSync(this.cacheDir);\n } catch {\n return result;\n }\n\n for (const shard of shards) {\n const shardPath = path.join(this.cacheDir, shard);\n let entries: string[];\n try {\n entries = readdirSync(shardPath);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".js\")) continue;\n const codePath = path.join(shardPath, entry);\n try {\n const stat = statSync(codePath);\n const mapPath = `${codePath}.map`;\n let mapSize = 0;\n try {\n mapSize = statSync(mapPath).size;\n } catch {\n // no map sidecar — fine\n }\n result.push({\n codePath,\n mapPath,\n size: stat.size + mapSize,\n mtimeMs: stat.mtimeMs,\n });\n } catch {\n // entry vanished mid-scan — skip\n }\n }\n }\n\n return result;\n }\n\n private evict(file: CacheFileInfo): void {\n try {\n rmSync(file.codePath, { force: true });\n } catch {\n // best effort\n }\n try {\n rmSync(file.mapPath, { force: true });\n } catch {\n // best effort\n }\n }\n\n /** Remove the entire cache directory (used by `--fresh`). */\n public clear(): void {\n try {\n rmSync(this.cacheDir, { recursive: true, force: true });\n } catch {\n // best effort\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region ../@warlock.js/core/src/dev-server/loader/version-registry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Version registry for the ESM loader hook.
|
|
4
|
+
*
|
|
5
|
+
* Lives in the hook worker thread. The main thread sends bump messages via
|
|
6
|
+
* MessageChannel; each bump increments the monotonic counter for that path,
|
|
7
|
+
* which causes `resolve()` to produce a new URL — Node sees a fresh module.
|
|
8
|
+
*
|
|
9
|
+
* Monotonic integers are used instead of timestamps so there is no clock-drift
|
|
10
|
+
* risk and the version is trivial to reason about during debugging.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Hook worker receives a bump from the file watcher:
|
|
14
|
+
* bumpVersion("/abs/path/to/user.model.ts");
|
|
15
|
+
* // → getVersion returns 1, resolve() appends ?v=1 to the URL
|
|
16
|
+
*/
|
|
17
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a filesystem path to a single canonical form so the main thread
|
|
20
|
+
* (which often holds forward-slash paths from posix-style joins) and the
|
|
21
|
+
* hook worker (which gets backslash paths from `fileURLToPath` on Windows)
|
|
22
|
+
* agree on map keys.
|
|
23
|
+
*/
|
|
24
|
+
function normalizeKey(absolutePath) {
|
|
25
|
+
return absolutePath.replace(/\\/g, "/").toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Increment the version counter for the given absolute path.
|
|
29
|
+
* Called when the file watcher detects a change.
|
|
30
|
+
*/
|
|
31
|
+
function bumpVersion(absolutePath) {
|
|
32
|
+
const key = normalizeKey(absolutePath);
|
|
33
|
+
versionMap.set(key, (versionMap.get(key) ?? 0) + 1);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Return the current version counter for the given absolute path.
|
|
37
|
+
* Returns 0 for paths not yet seen (first import).
|
|
38
|
+
*/
|
|
39
|
+
function getVersion(absolutePath) {
|
|
40
|
+
return versionMap.get(normalizeKey(absolutePath)) ?? 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { bumpVersion, getVersion };
|
|
45
|
+
//# sourceMappingURL=version-registry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-registry.mjs","names":[],"sources":["../../../../../../../../@warlock.js/core/src/dev-server/loader/version-registry.ts"],"sourcesContent":["/**\n * Version registry for the ESM loader hook.\n *\n * Lives in the hook worker thread. The main thread sends bump messages via\n * MessageChannel; each bump increments the monotonic counter for that path,\n * which causes `resolve()` to produce a new URL — Node sees a fresh module.\n *\n * Monotonic integers are used instead of timestamps so there is no clock-drift\n * risk and the version is trivial to reason about during debugging.\n *\n * @example\n * // Hook worker receives a bump from the file watcher:\n * bumpVersion(\"/abs/path/to/user.model.ts\");\n * // → getVersion returns 1, resolve() appends ?v=1 to the URL\n */\n\nconst versionMap = new Map<string, number>();\n\n/**\n * Normalize a filesystem path to a single canonical form so the main thread\n * (which often holds forward-slash paths from posix-style joins) and the\n * hook worker (which gets backslash paths from `fileURLToPath` on Windows)\n * agree on map keys.\n */\nfunction normalizeKey(absolutePath: string): string {\n return absolutePath.replace(/\\\\/g, \"/\").toLowerCase();\n}\n\n/**\n * Increment the version counter for the given absolute path.\n * Called when the file watcher detects a change.\n */\nexport function bumpVersion(absolutePath: string): void {\n const key = normalizeKey(absolutePath);\n versionMap.set(key, (versionMap.get(key) ?? 0) + 1);\n}\n\n/**\n * Return the current version counter for the given absolute path.\n * Returns 0 for paths not yet seen (first import).\n */\nexport function getVersion(absolutePath: string): number {\n return versionMap.get(normalizeKey(absolutePath)) ?? 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,MAAM,6BAAa,IAAI,IAAoB;;;;;;;AAQ3C,SAAS,aAAa,cAA8B;CAClD,OAAO,aAAa,QAAQ,OAAO,GAAG,CAAC,CAAC,YAAY;AACtD;;;;;AAMA,SAAgB,YAAY,cAA4B;CACtD,MAAM,MAAM,aAAa,YAAY;CACrC,WAAW,IAAI,MAAM,WAAW,IAAI,GAAG,KAAK,KAAK,CAAC;AACpD;;;;;AAMA,SAAgB,WAAW,cAA8B;CACvD,OAAO,WAAW,IAAI,aAAa,YAAY,CAAC,KAAK;AACvD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rootPath, srcPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
3
|
import { getWarlockVersion } from "../utils/framework-vesion.mjs";
|
|
4
|
-
import { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, communicatorsConfigStub, notificationMigrationStub, notificationModelStub, notificationsConfigStub, socketConfigStub } from "./stubs.mjs";
|
|
4
|
+
import { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, communicatorsConfigStub, notificationControllersStub, notificationMigrationStub, notificationModelStub, notificationRoutesStub, notificationsConfigStub, socketConfigStub } from "./stubs.mjs";
|
|
5
5
|
import { colors } from "@mongez/copper";
|
|
6
6
|
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, getJsonFileAsync, putFileAsync, putJsonFileAsync } from "@warlock.js/fs";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
@@ -137,6 +137,10 @@ async function completeNotificationsInstallation(_options) {
|
|
|
137
137
|
const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;
|
|
138
138
|
await putFileAsync(srcPath("app/notifications/migrations", migrationFile), notificationMigrationStub);
|
|
139
139
|
console.log(`${colors.green("✓")} Created src/app/notifications/migrations/${migrationFile}`);
|
|
140
|
+
await ensureDirectoryAsync(srcPath("app/notifications/controllers"));
|
|
141
|
+
await putFileAsync(srcPath("app/notifications/controllers/notifications.controller.ts"), notificationControllersStub);
|
|
142
|
+
await putFileAsync(srcPath("app/notifications/routes.ts"), notificationRoutesStub);
|
|
143
|
+
console.log(`${colors.green("✓")} Created src/app/notifications/routes.ts + controllers`);
|
|
140
144
|
}
|
|
141
145
|
async function registerAccessLocale() {
|
|
142
146
|
const localesPath = srcPath("app/shared/utils/locales.ts");
|
|
@@ -296,9 +300,12 @@ const featuresMap = {
|
|
|
296
300
|
}
|
|
297
301
|
},
|
|
298
302
|
notifications: {
|
|
299
|
-
description: "Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration into src/app/notifications",
|
|
303
|
+
description: "Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications",
|
|
300
304
|
requires: ["mail"],
|
|
301
|
-
dependencies: {
|
|
305
|
+
dependencies: {
|
|
306
|
+
"@warlock.js/notifications": "~4.0.0",
|
|
307
|
+
"@warlock.js/auth": "~4.0.0"
|
|
308
|
+
},
|
|
302
309
|
ejectConfig: {
|
|
303
310
|
content: notificationsConfigStub,
|
|
304
311
|
name: "notifications"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport {\r\n accessConfigStub,\r\n accessResolverStub,\r\n accessRoleMigrationStub,\r\n accessRoleModelIndexStub,\r\n accessRoleModelStub,\r\n accessUserRoleMigrationStub,\r\n accessUserRoleModelIndexStub,\r\n accessUserRoleModelStub,\r\n communicatorsConfigStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationsConfigStub,\r\n socketConfigStub,\r\n} from \"./stubs\";\r\n\r\n/**\r\n * Build a migration filename timestamp prefix in the framework's\r\n * MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this\r\n * prefix and orders migrations deterministically by it. Pass `offsetSeconds` to\r\n * stamp sibling migrations created in the same scaffold a second apart so they\r\n * never collide and keep a stable relative order.\r\n */\r\nfunction migrationTimestamp(offsetSeconds = 0): string {\r\n const now = new Date(Date.now() + offsetSeconds * 1000);\r\n const pad = (value: number) => String(value).padStart(2, \"0\");\r\n\r\n return (\r\n `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_` +\r\n `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`\r\n );\r\n}\r\n\r\nasync function completeTestInstallation(options: CommandActionData) {\r\n // Create test-global-setup.ts (runs once before all tests)\r\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\r\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\r\n\r\n if (!testGlobalSetupExists) {\r\n await putFileAsync(\r\n testGlobalSetupPath,\r\n `/**\r\n * Global Test Setup\r\n *\r\n * Runs ONCE before all test workers.\r\n * Starts the HTTP server for integration tests.\r\n */\r\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core\";\r\n\r\nexport async function setup() {\r\n await startHttpTestServer();\r\n}\r\n\r\nexport async function teardown() {\r\n await stopHttpTestServer();\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\r\n }\r\n\r\n // Create test-setup.ts (runs per worker thread)\r\n const testSetupPath = srcPath(\"test-setup.ts\");\r\n const testSetupExists = await fileExistsAsync(testSetupPath);\r\n\r\n if (!testSetupExists) {\r\n await putFileAsync(\r\n testSetupPath,\r\n `/**\r\n * Per-Worker Test Setup\r\n *\r\n * Runs in EACH Vitest worker thread before tests execute.\r\n * Sets up per-worker database and cache connections.\r\n */\r\nimport { setupTest } from \"@warlock.js/core\";\r\n\r\nawait setupTest({ connectors: true });\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\r\n }\r\n\r\n // Create vite.config.ts\r\n const viteConfigPath = rootPath(\"vite.config.ts\");\r\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\r\n\r\n if (!viteConfigExists) {\r\n await putFileAsync(\r\n viteConfigPath,\r\n `import { lowerStage3Decorators } from \"@warlock.js/core\";\r\nimport mongezVite from \"@mongez/vite\";\r\nimport { defineConfig } from \"vitest/config\";\r\n\r\nexport default defineConfig({\r\n // lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)\r\n // decorators with esbuild before oxc / the SSR rewrite can mangle them, so\r\n // decorated Cascade models load under Vitest.\r\n plugins: [lowerStage3Decorators(), mongezVite()],\r\n test: {\r\n globalSetup: \"./src/test-global-setup.ts\", // HTTP server - runs once\r\n setupFiles: [\"./src/test-setup.ts\"], // DB/cache - runs per worker\r\n environment: \"node\",\r\n globals: false,\r\n include: [\"src/app/**/*.test.ts\"],\r\n },\r\n});\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\r\n }\r\n}\r\n\r\nasync function completeReactEmailInstallation(_options: CommandActionData) {\r\n // 1. Create emails/ folder with a sample component\r\n const emailsFolderPath = rootPath(\"emails\");\r\n const sampleEmailPath = rootPath(\"emails/welcome-email.tsx\");\r\n\r\n if (!(await fileExistsAsync(sampleEmailPath))) {\r\n await ensureDirectoryAsync(emailsFolderPath);\r\n await putFileAsync(\r\n sampleEmailPath,\r\n `import { Body, Container, Head, Html, Text } from \"@react-email/components\";\r\nimport { Tailwind } from \"@react-email/tailwind\";\r\n\r\ninterface WelcomeEmailProps {\r\n name: string;\r\n}\r\n\r\n/**\r\n * Sample welcome email component.\r\n * Preview with: yarn email:preview\r\n */\r\nexport default function WelcomeEmail({ name }: WelcomeEmailProps) {\r\n return (\r\n <Html>\r\n <Head />\r\n <Tailwind>\r\n <Body className=\"bg-gray-100 font-sans\">\r\n <Container className=\"mx-auto max-w-xl py-8 px-4\">\r\n <Text className=\"text-2xl font-bold text-gray-900\">\r\n Welcome, {name}!\r\n </Text>\r\n <Text className=\"text-gray-600 mt-2\">\r\n You're all set. We're glad to have you on board.\r\n </Text>\r\n </Container>\r\n </Body>\r\n </Tailwind>\r\n </Html>\r\n );\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created emails/welcome-email.tsx`);\r\n }\r\n\r\n // 2. Patch tsconfig.json — add \"emails\" to include if missing\r\n const tsconfigPath = rootPath(\"tsconfig.json\");\r\n const tsconfig = await getJsonFileAsync(tsconfigPath);\r\n\r\n if (!tsconfig.include) {\r\n tsconfig.include = [];\r\n }\r\n\r\n if (!tsconfig.include.includes(\"emails\")) {\r\n tsconfig.include.push(\"emails\");\r\n await putJsonFileAsync(tsconfigPath, tsconfig);\r\n console.log(`${colors.green(\"✓\")} Added \"emails\" to tsconfig.json include`);\r\n }\r\n}\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n}\r\n\r\nasync function registerAccessLocale() {\r\n // Register the access locale in the project's shared translations file so a\r\n // denied check returns a real sentence, not the raw \"access.errors.forbidden\"\r\n // key. Append when the file exists, create it otherwise; skip if already there.\r\n const localesPath = srcPath(\"app/shared/utils/locales.ts\");\r\n\r\n const accessLocale = `groupedTranslations(\"access\", {\r\n errors: {\r\n forbidden: {\r\n en: \"You do not have permission to perform this action.\",\r\n ar: \"ليس لديك صلاحية لتنفيذ هذا الإجراء.\",\r\n },\r\n },\r\n});\r\n`;\r\n\r\n if (await fileExistsAsync(localesPath)) {\r\n const current = await getFileAsync(localesPath);\r\n\r\n if (current.includes(`groupedTranslations(\"access\"`)) {\r\n console.log(`${colors.yellowBright(\"access\")} locale already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n // The file uses groupedTranslations already iff it calls it — only inject the\r\n // import when no call is present yet.\r\n const importLine = `import { groupedTranslations } from \"@warlock.js/core\";`;\r\n const prefix = current.includes(\"groupedTranslations(\") ? \"\" : `${importLine}\\n\\n`;\r\n\r\n await putFileAsync(localesPath, `${prefix}${current.trimEnd()}\\n\\n${accessLocale}`);\r\n\r\n console.log(\r\n `${colors.green(\"✓\")} Registered the access locale in src/app/shared/utils/locales.ts`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/shared/utils\"));\r\n\r\n await putFileAsync(\r\n localesPath,\r\n `import { groupedTranslations } from \"@warlock.js/core\";\\n\\n${accessLocale}`,\r\n );\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/app/shared/utils/locales.ts with the access locale`);\r\n}\r\n\r\nasync function scaffoldAccessFiles() {\r\n // The resolver file is the sentinel for \"access already scaffolded\" — its\r\n // presence means the role/user-role model folders and their timestamped\r\n // migrations were created too, so we must not re-emit duplicate migrations on\r\n // a second run.\r\n const resolverPath = srcPath(\"app/access/services/access-resolver.ts\");\r\n\r\n if (await fileExistsAsync(resolverPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/access\")} already scaffolded, skipping resolver + role tables...`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n // 1. Role catalog model folder (model + barrel + migration). The catalog row\r\n // is role name → granted permissions; managed at runtime in the DB.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role\"));\r\n await putFileAsync(srcPath(\"app/access/models/role/role.model.ts\"), accessRoleModelStub);\r\n await putFileAsync(srcPath(\"app/access/models/role/index.ts\"), accessRoleModelIndexStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role/migrations\"));\r\n\r\n // Migration filenames carry a MM-DD-YYYY_HH-MM-SS prefix so cascade infers\r\n // their createdAt and orders them deterministically (the migrate action\r\n // discovers src/app/*/models/*/migrations/*). The two tables are independent\r\n // (no FK between them), but the user-role migration is stamped a second later\r\n // so the relative order is stable.\r\n const roleMigrationFile = `${migrationTimestamp()}-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/role/migrations\", roleMigrationFile),\r\n accessRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/role/migrations/${roleMigrationFile}`,\r\n );\r\n\r\n // 2. UserRole assignment model folder (model + barrel + migration). The model\r\n // statics scope an unresolved tenant to GLOBAL rows only (security\r\n // invariant) — see the stub for the reasoning.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role\"));\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/user-role.model.ts\"),\r\n accessUserRoleModelStub,\r\n );\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/index.ts\"),\r\n accessUserRoleModelIndexStub,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/user-role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role/migrations\"));\r\n\r\n const userRoleMigrationFile = `${migrationTimestamp(1)}-user-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/migrations\", userRoleMigrationFile),\r\n accessUserRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/user-role/migrations/${userRoleMigrationFile}`,\r\n );\r\n\r\n // 3. The DatabaseAccessResolver — the one required config seam, wired into\r\n // config/access.ts by the ejected stub.\r\n await ensureDirectoryAsync(srcPath(\"app/access/services\"));\r\n await putFileAsync(resolverPath, accessResolverStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/services/access-resolver.ts`);\r\n}\r\n\r\nasync function completeAccessInstallation(_options: CommandActionData) {\r\n await registerAccessLocale();\r\n await scaffoldAccessFiles();\r\n}\r\n\r\nconst featuresMap: Record<\r\n string,\r\n {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n }\r\n> = {\r\n \"react-email\": {\r\n description: \"Installs react-email for building email templates with React and Tailwind\",\r\n requires: [\"mail\", \"react\"],\r\n dependencies: {\r\n \"react-email\": \"^5.2.10\",\r\n \"@react-email/components\": \"^1.0.11\",\r\n \"@react-email/render\": \"^2.0.5\",\r\n \"@react-email/tailwind\": \"^2.0.7\",\r\n },\r\n devDependencies: {\r\n \"@react-email/preview-server\": \"5.2.10\",\r\n },\r\n script: {\r\n \"email:preview\": \"npx react-email dev\",\r\n },\r\n onExecuting: completeReactEmailInstallation,\r\n },\r\n react: {\r\n description:\r\n \"Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML\",\r\n dependencies: {\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n },\r\n },\r\n image: {\r\n description: \"Installs sharp for image processing\",\r\n dependencies: {\r\n sharp: \"^0.34.5\",\r\n },\r\n },\r\n mail: {\r\n description: \"Installs nodemailer for sending emails\",\r\n dependencies: {\r\n nodemailer: \"^8.0.5\",\r\n },\r\n devDependencies: {\r\n \"@types/nodemailer\": \"^8.0.0\",\r\n },\r\n },\r\n ses: {\r\n description: \"Installs AWS SES SDK for sending emails via Amazon SES\",\r\n dependencies: {\r\n \"@aws-sdk/client-sesv2\": \"^3.1025.0\",\r\n },\r\n },\r\n mongodb: {\r\n description: \"Installs mongodb driver for database driver (Cascade Package)\",\r\n dependencies: {\r\n mongodb: \"^7.0.0\",\r\n },\r\n },\r\n scheduler: {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": \"~4.0.0\",\r\n },\r\n },\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: {\r\n description: \"Installs pg for Postgres database (Cascade Package)\",\r\n dependencies: {\r\n pg: \"^8.11.0\",\r\n },\r\n },\r\n mysql: {\r\n description: \"Installs mysql2 for MySQL database driver (Cascade Package)\",\r\n dependencies: {\r\n mysql2: \"^3.5.0\",\r\n },\r\n },\r\n redis: {\r\n description: \"Installs redis for Redis cache driver (Cache Package)\",\r\n dependencies: {\r\n redis: \"^4.6.13\",\r\n },\r\n },\r\n s3: {\r\n description: \"Installs AWS SDK for Cloud storage (Storage Package)\",\r\n dependencies: {\r\n \"@aws-sdk/client-s3\": \"^3.955.0\",\r\n \"@aws-sdk/lib-storage\": \"^3.955.0\",\r\n \"@aws-sdk/s3-request-presigner\": \"^3.955.0\",\r\n },\r\n },\r\n test: {\r\n description: \"Installs warlock test for testing\",\r\n onExecuting: completeTestInstallation,\r\n script: {\r\n test: \"vitest run\",\r\n \"test:coverage\": \"vitest run --coverage\",\r\n \"test:ui\": \"vitest --ui\",\r\n \"test:watch\": \"vitest --watch\",\r\n },\r\n devDependencies: {\r\n \"@mongez/vite\": \"^2.0.4\",\r\n vite: \"^8.0.16\",\r\n vitest: \"^4.1.8\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n },\r\n },\r\n herald: {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": \"~4.0.0\",\r\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n },\r\n socket: {\r\n description: \"Installs socket.io for the realtime socket server (Socket Connector)\",\r\n dependencies: {\r\n \"socket.io\": \"^4.8.3\",\r\n },\r\n ejectConfig: {\r\n content: socketConfigStub,\r\n name: \"socket\",\r\n },\r\n },\r\n notifications: {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default, which needs\r\n // nodemailer — pulled in via the `mail` feature.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n },\r\n access: {\r\n description:\r\n \"Installs @warlock.js/access — authorization (RBAC + ABAC): permission checks, ABAC policies, and roles. Ejects config/access.ts, the DatabaseAccessResolver + Role/UserRole models and migrations into src/app/access, and registers the access locale in src/app/shared/utils/locales.ts\",\r\n dependencies: {\r\n \"@warlock.js/access\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: accessConfigStub,\r\n name: \"access\",\r\n },\r\n onExecuting: completeAccessInstallation,\r\n },\r\n ai: {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": \"~4.0.0\",\r\n },\r\n },\r\n openai: {\r\n description: \"Installs the OpenAI provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-openai\": \"~4.0.0\",\r\n },\r\n },\r\n google: {\r\n description: \"Installs the Google (Gemini) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-google\": \"~4.0.0\",\r\n },\r\n },\r\n anthropic: {\r\n description: \"Installs the Anthropic (Claude) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-anthropic\": \"~4.0.0\",\r\n },\r\n },\r\n bedrock: {\r\n description: \"Installs the AWS Bedrock provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-bedrock\": \"~4.0.0\",\r\n },\r\n },\r\n ollama: {\r\n description: \"Installs the Ollama provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-ollama\": \"~4.0.0\",\r\n },\r\n },\r\n};\r\n\r\nconst allowedFeatures = Object.keys(featuresMap);\r\n\r\ntype PackageManager = \"yarn\" | \"pnpm\" | \"npm\";\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(featuresMap[feature].description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n\r\n const currentPackageJson = await getJsonFileAsync(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(packageManager as PackageManager, dependencies, devDependencies);\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n if (featurePackages.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nasync function installDependencies(\r\n packageManager: PackageManager,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n const packageManagerCommand = await getPackageManagerCommand(packageManager);\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(dependencies).join(\" \")}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(devDependencies).join(\" \")} -D`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n\r\nasync function getPackageManagerCommand(packageManager?: PackageManager) {\r\n if (!packageManager) {\r\n // try to detect it through checking lock files\r\n packageManager = await detectPackageManager();\r\n }\r\n\r\n if (packageManager === \"npm\") {\r\n return \"npm install\";\r\n }\r\n\r\n if (packageManager === \"yarn\") {\r\n return \"yarn add\";\r\n }\r\n\r\n if (packageManager === \"pnpm\") {\r\n return \"pnpm add\";\r\n }\r\n}\r\n\r\nasync function detectPackageManager() {\r\n if (await fileExistsAsync(rootPath(\"package-lock.json\"))) {\r\n return \"npm\";\r\n }\r\n\r\n if (await fileExistsAsync(rootPath(\"yarn.lock\"))) {\r\n return \"yarn\";\r\n }\r\n\r\n if (await fileExistsAsync(rootPath(\"pnpm-lock.yaml\"))) {\r\n return \"pnpm\";\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../@warlock.js/core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport {\r\n accessConfigStub,\r\n accessResolverStub,\r\n accessRoleMigrationStub,\r\n accessRoleModelIndexStub,\r\n accessRoleModelStub,\r\n accessUserRoleMigrationStub,\r\n accessUserRoleModelIndexStub,\r\n accessUserRoleModelStub,\r\n communicatorsConfigStub,\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n socketConfigStub,\r\n} from \"./stubs\";\r\n\r\n/**\r\n * Build a migration filename timestamp prefix in the framework's\r\n * MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this\r\n * prefix and orders migrations deterministically by it. Pass `offsetSeconds` to\r\n * stamp sibling migrations created in the same scaffold a second apart so they\r\n * never collide and keep a stable relative order.\r\n */\r\nfunction migrationTimestamp(offsetSeconds = 0): string {\r\n const now = new Date(Date.now() + offsetSeconds * 1000);\r\n const pad = (value: number) => String(value).padStart(2, \"0\");\r\n\r\n return (\r\n `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_` +\r\n `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`\r\n );\r\n}\r\n\r\nasync function completeTestInstallation(options: CommandActionData) {\r\n // Create test-global-setup.ts (runs once before all tests)\r\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\r\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\r\n\r\n if (!testGlobalSetupExists) {\r\n await putFileAsync(\r\n testGlobalSetupPath,\r\n `/**\r\n * Global Test Setup\r\n *\r\n * Runs ONCE before all test workers.\r\n * Starts the HTTP server for integration tests.\r\n */\r\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core\";\r\n\r\nexport async function setup() {\r\n await startHttpTestServer();\r\n}\r\n\r\nexport async function teardown() {\r\n await stopHttpTestServer();\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\r\n }\r\n\r\n // Create test-setup.ts (runs per worker thread)\r\n const testSetupPath = srcPath(\"test-setup.ts\");\r\n const testSetupExists = await fileExistsAsync(testSetupPath);\r\n\r\n if (!testSetupExists) {\r\n await putFileAsync(\r\n testSetupPath,\r\n `/**\r\n * Per-Worker Test Setup\r\n *\r\n * Runs in EACH Vitest worker thread before tests execute.\r\n * Sets up per-worker database and cache connections.\r\n */\r\nimport { setupTest } from \"@warlock.js/core\";\r\n\r\nawait setupTest({ connectors: true });\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\r\n }\r\n\r\n // Create vite.config.ts\r\n const viteConfigPath = rootPath(\"vite.config.ts\");\r\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\r\n\r\n if (!viteConfigExists) {\r\n await putFileAsync(\r\n viteConfigPath,\r\n `import { lowerStage3Decorators } from \"@warlock.js/core\";\r\nimport mongezVite from \"@mongez/vite\";\r\nimport { defineConfig } from \"vitest/config\";\r\n\r\nexport default defineConfig({\r\n // lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)\r\n // decorators with esbuild before oxc / the SSR rewrite can mangle them, so\r\n // decorated Cascade models load under Vitest.\r\n plugins: [lowerStage3Decorators(), mongezVite()],\r\n test: {\r\n globalSetup: \"./src/test-global-setup.ts\", // HTTP server - runs once\r\n setupFiles: [\"./src/test-setup.ts\"], // DB/cache - runs per worker\r\n environment: \"node\",\r\n globals: false,\r\n include: [\"src/app/**/*.test.ts\"],\r\n },\r\n});\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\r\n }\r\n}\r\n\r\nasync function completeReactEmailInstallation(_options: CommandActionData) {\r\n // 1. Create emails/ folder with a sample component\r\n const emailsFolderPath = rootPath(\"emails\");\r\n const sampleEmailPath = rootPath(\"emails/welcome-email.tsx\");\r\n\r\n if (!(await fileExistsAsync(sampleEmailPath))) {\r\n await ensureDirectoryAsync(emailsFolderPath);\r\n await putFileAsync(\r\n sampleEmailPath,\r\n `import { Body, Container, Head, Html, Text } from \"@react-email/components\";\r\nimport { Tailwind } from \"@react-email/tailwind\";\r\n\r\ninterface WelcomeEmailProps {\r\n name: string;\r\n}\r\n\r\n/**\r\n * Sample welcome email component.\r\n * Preview with: yarn email:preview\r\n */\r\nexport default function WelcomeEmail({ name }: WelcomeEmailProps) {\r\n return (\r\n <Html>\r\n <Head />\r\n <Tailwind>\r\n <Body className=\"bg-gray-100 font-sans\">\r\n <Container className=\"mx-auto max-w-xl py-8 px-4\">\r\n <Text className=\"text-2xl font-bold text-gray-900\">\r\n Welcome, {name}!\r\n </Text>\r\n <Text className=\"text-gray-600 mt-2\">\r\n You're all set. We're glad to have you on board.\r\n </Text>\r\n </Container>\r\n </Body>\r\n </Tailwind>\r\n </Html>\r\n );\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created emails/welcome-email.tsx`);\r\n }\r\n\r\n // 2. Patch tsconfig.json — add \"emails\" to include if missing\r\n const tsconfigPath = rootPath(\"tsconfig.json\");\r\n const tsconfig = await getJsonFileAsync(tsconfigPath);\r\n\r\n if (!tsconfig.include) {\r\n tsconfig.include = [];\r\n }\r\n\r\n if (!tsconfig.include.includes(\"emails\")) {\r\n tsconfig.include.push(\"emails\");\r\n await putJsonFileAsync(tsconfigPath, tsconfig);\r\n console.log(`${colors.green(\"✓\")} Added \"emails\" to tsconfig.json include`);\r\n }\r\n}\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nasync function registerAccessLocale() {\r\n // Register the access locale in the project's shared translations file so a\r\n // denied check returns a real sentence, not the raw \"access.errors.forbidden\"\r\n // key. Append when the file exists, create it otherwise; skip if already there.\r\n const localesPath = srcPath(\"app/shared/utils/locales.ts\");\r\n\r\n const accessLocale = `groupedTranslations(\"access\", {\r\n errors: {\r\n forbidden: {\r\n en: \"You do not have permission to perform this action.\",\r\n ar: \"ليس لديك صلاحية لتنفيذ هذا الإجراء.\",\r\n },\r\n },\r\n});\r\n`;\r\n\r\n if (await fileExistsAsync(localesPath)) {\r\n const current = await getFileAsync(localesPath);\r\n\r\n if (current.includes(`groupedTranslations(\"access\"`)) {\r\n console.log(`${colors.yellowBright(\"access\")} locale already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n // The file uses groupedTranslations already iff it calls it — only inject the\r\n // import when no call is present yet.\r\n const importLine = `import { groupedTranslations } from \"@warlock.js/core\";`;\r\n const prefix = current.includes(\"groupedTranslations(\") ? \"\" : `${importLine}\\n\\n`;\r\n\r\n await putFileAsync(localesPath, `${prefix}${current.trimEnd()}\\n\\n${accessLocale}`);\r\n\r\n console.log(\r\n `${colors.green(\"✓\")} Registered the access locale in src/app/shared/utils/locales.ts`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/shared/utils\"));\r\n\r\n await putFileAsync(\r\n localesPath,\r\n `import { groupedTranslations } from \"@warlock.js/core\";\\n\\n${accessLocale}`,\r\n );\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/app/shared/utils/locales.ts with the access locale`);\r\n}\r\n\r\nasync function scaffoldAccessFiles() {\r\n // The resolver file is the sentinel for \"access already scaffolded\" — its\r\n // presence means the role/user-role model folders and their timestamped\r\n // migrations were created too, so we must not re-emit duplicate migrations on\r\n // a second run.\r\n const resolverPath = srcPath(\"app/access/services/access-resolver.ts\");\r\n\r\n if (await fileExistsAsync(resolverPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/access\")} already scaffolded, skipping resolver + role tables...`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n // 1. Role catalog model folder (model + barrel + migration). The catalog row\r\n // is role name → granted permissions; managed at runtime in the DB.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role\"));\r\n await putFileAsync(srcPath(\"app/access/models/role/role.model.ts\"), accessRoleModelStub);\r\n await putFileAsync(srcPath(\"app/access/models/role/index.ts\"), accessRoleModelIndexStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role/migrations\"));\r\n\r\n // Migration filenames carry a MM-DD-YYYY_HH-MM-SS prefix so cascade infers\r\n // their createdAt and orders them deterministically (the migrate action\r\n // discovers src/app/*/models/*/migrations/*). The two tables are independent\r\n // (no FK between them), but the user-role migration is stamped a second later\r\n // so the relative order is stable.\r\n const roleMigrationFile = `${migrationTimestamp()}-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/role/migrations\", roleMigrationFile),\r\n accessRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/role/migrations/${roleMigrationFile}`,\r\n );\r\n\r\n // 2. UserRole assignment model folder (model + barrel + migration). The model\r\n // statics scope an unresolved tenant to GLOBAL rows only (security\r\n // invariant) — see the stub for the reasoning.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role\"));\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/user-role.model.ts\"),\r\n accessUserRoleModelStub,\r\n );\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/index.ts\"),\r\n accessUserRoleModelIndexStub,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/user-role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role/migrations\"));\r\n\r\n const userRoleMigrationFile = `${migrationTimestamp(1)}-user-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/migrations\", userRoleMigrationFile),\r\n accessUserRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/user-role/migrations/${userRoleMigrationFile}`,\r\n );\r\n\r\n // 3. The DatabaseAccessResolver — the one required config seam, wired into\r\n // config/access.ts by the ejected stub.\r\n await ensureDirectoryAsync(srcPath(\"app/access/services\"));\r\n await putFileAsync(resolverPath, accessResolverStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/services/access-resolver.ts`);\r\n}\r\n\r\nasync function completeAccessInstallation(_options: CommandActionData) {\r\n await registerAccessLocale();\r\n await scaffoldAccessFiles();\r\n}\r\n\r\nconst featuresMap: Record<\r\n string,\r\n {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n }\r\n> = {\r\n \"react-email\": {\r\n description: \"Installs react-email for building email templates with React and Tailwind\",\r\n requires: [\"mail\", \"react\"],\r\n dependencies: {\r\n \"react-email\": \"^5.2.10\",\r\n \"@react-email/components\": \"^1.0.11\",\r\n \"@react-email/render\": \"^2.0.5\",\r\n \"@react-email/tailwind\": \"^2.0.7\",\r\n },\r\n devDependencies: {\r\n \"@react-email/preview-server\": \"5.2.10\",\r\n },\r\n script: {\r\n \"email:preview\": \"npx react-email dev\",\r\n },\r\n onExecuting: completeReactEmailInstallation,\r\n },\r\n react: {\r\n description:\r\n \"Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML\",\r\n dependencies: {\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n },\r\n },\r\n image: {\r\n description: \"Installs sharp for image processing\",\r\n dependencies: {\r\n sharp: \"^0.34.5\",\r\n },\r\n },\r\n mail: {\r\n description: \"Installs nodemailer for sending emails\",\r\n dependencies: {\r\n nodemailer: \"^8.0.5\",\r\n },\r\n devDependencies: {\r\n \"@types/nodemailer\": \"^8.0.0\",\r\n },\r\n },\r\n ses: {\r\n description: \"Installs AWS SES SDK for sending emails via Amazon SES\",\r\n dependencies: {\r\n \"@aws-sdk/client-sesv2\": \"^3.1025.0\",\r\n },\r\n },\r\n mongodb: {\r\n description: \"Installs mongodb driver for database driver (Cascade Package)\",\r\n dependencies: {\r\n mongodb: \"^7.0.0\",\r\n },\r\n },\r\n scheduler: {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": \"~4.0.0\",\r\n },\r\n },\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: {\r\n description: \"Installs pg for Postgres database (Cascade Package)\",\r\n dependencies: {\r\n pg: \"^8.11.0\",\r\n },\r\n },\r\n mysql: {\r\n description: \"Installs mysql2 for MySQL database driver (Cascade Package)\",\r\n dependencies: {\r\n mysql2: \"^3.5.0\",\r\n },\r\n },\r\n redis: {\r\n description: \"Installs redis for Redis cache driver (Cache Package)\",\r\n dependencies: {\r\n redis: \"^4.6.13\",\r\n },\r\n },\r\n s3: {\r\n description: \"Installs AWS SDK for Cloud storage (Storage Package)\",\r\n dependencies: {\r\n \"@aws-sdk/client-s3\": \"^3.955.0\",\r\n \"@aws-sdk/lib-storage\": \"^3.955.0\",\r\n \"@aws-sdk/s3-request-presigner\": \"^3.955.0\",\r\n },\r\n },\r\n test: {\r\n description: \"Installs warlock test for testing\",\r\n onExecuting: completeTestInstallation,\r\n script: {\r\n test: \"vitest run\",\r\n \"test:coverage\": \"vitest run --coverage\",\r\n \"test:ui\": \"vitest --ui\",\r\n \"test:watch\": \"vitest --watch\",\r\n },\r\n devDependencies: {\r\n \"@mongez/vite\": \"^2.0.4\",\r\n vite: \"^8.0.16\",\r\n vitest: \"^4.1.8\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n },\r\n },\r\n herald: {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": \"~4.0.0\",\r\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n },\r\n socket: {\r\n description: \"Installs socket.io for the realtime socket server (Socket Connector)\",\r\n dependencies: {\r\n \"socket.io\": \"^4.8.3\",\r\n },\r\n ejectConfig: {\r\n content: socketConfigStub,\r\n name: \"socket\",\r\n },\r\n },\r\n notifications: {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": \"~4.0.0\",\r\n \"@warlock.js/auth\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n },\r\n access: {\r\n description:\r\n \"Installs @warlock.js/access — authorization (RBAC + ABAC): permission checks, ABAC policies, and roles. Ejects config/access.ts, the DatabaseAccessResolver + Role/UserRole models and migrations into src/app/access, and registers the access locale in src/app/shared/utils/locales.ts\",\r\n dependencies: {\r\n \"@warlock.js/access\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: accessConfigStub,\r\n name: \"access\",\r\n },\r\n onExecuting: completeAccessInstallation,\r\n },\r\n ai: {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": \"~4.0.0\",\r\n },\r\n },\r\n openai: {\r\n description: \"Installs the OpenAI provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-openai\": \"~4.0.0\",\r\n },\r\n },\r\n google: {\r\n description: \"Installs the Google (Gemini) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-google\": \"~4.0.0\",\r\n },\r\n },\r\n anthropic: {\r\n description: \"Installs the Anthropic (Claude) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-anthropic\": \"~4.0.0\",\r\n },\r\n },\r\n bedrock: {\r\n description: \"Installs the AWS Bedrock provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-bedrock\": \"~4.0.0\",\r\n },\r\n },\r\n ollama: {\r\n description: \"Installs the Ollama provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-ollama\": \"~4.0.0\",\r\n },\r\n },\r\n};\r\n\r\nconst allowedFeatures = Object.keys(featuresMap);\r\n\r\ntype PackageManager = \"yarn\" | \"pnpm\" | \"npm\";\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(featuresMap[feature].description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n\r\n const currentPackageJson = await getJsonFileAsync(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(packageManager as PackageManager, dependencies, devDependencies);\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n if (featurePackages.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nasync function installDependencies(\r\n packageManager: PackageManager,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n const packageManagerCommand = await getPackageManagerCommand(packageManager);\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(dependencies).join(\" \")}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(devDependencies).join(\" \")} -D`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n\r\nasync function getPackageManagerCommand(packageManager?: PackageManager) {\r\n if (!packageManager) {\r\n // try to detect it through checking lock files\r\n packageManager = await detectPackageManager();\r\n }\r\n\r\n if (packageManager === \"npm\") {\r\n return \"npm install\";\r\n }\r\n\r\n if (packageManager === \"yarn\") {\r\n return \"yarn add\";\r\n }\r\n\r\n if (packageManager === \"pnpm\") {\r\n return \"pnpm add\";\r\n }\r\n}\r\n\r\nasync function detectPackageManager() {\r\n if (await fileExistsAsync(rootPath(\"package-lock.json\"))) {\r\n return \"npm\";\r\n }\r\n\r\n if (await fileExistsAsync(rootPath(\"yarn.lock\"))) {\r\n return \"yarn\";\r\n }\r\n\r\n if (await fileExistsAsync(rootPath(\"pnpm-lock.yaml\"))) {\r\n return \"pnpm\";\r\n }\r\n}\r\n"],"mappings":"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"}
|