@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.10
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/.build-stamp +1 -1
- package/{CodeAgent-kyprmy06.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-21mtze3j.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-5s780vnz.js → application.command-m1kcd7jf.js} +54 -42
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
- package/buildBatch.proc.js +24 -15
- package/{cloud.command-yj6xb2a7.js → cloud.command-9cp1t0f5.js} +60 -67
- package/cloud.script-dgstqt1m.js +24 -0
- package/{code.command-3wkn4ja5.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-9jjn3gcw.js → context.command-0qx447mb.js} +17 -13
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +3 -3
- package/guidelines/conventions/conventions.instruction.md +457 -1071
- package/guidelines/cssRule/cssRule.instruction.md +28 -7
- package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +43 -20
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +44 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +135 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
- package/incrementalBuilder.proc.js +10 -7
- package/index-192ha4xh.js +1173 -0
- package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
- package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
- package/{index-rtct7d43.js → index-20bxxyvn.js} +80 -80
- package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
- package/{index-8r4d7cmt.js → index-4mhy6qme.js} +4 -4
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/index-665zp9rs.js +271 -0
- package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
- package/{index-krq7btbt.js → index-8fp3b47h.js} +419 -418
- package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
- package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-rc5rwyps.js → index-bspm21y1.js} +71 -60
- package/index-cpb3nvma.js +667 -0
- package/{index-jx4bzvwg.js → index-d47ypebv.js} +97 -55
- package/{index-ae5pwhgs.js → index-d72hfmdm.js} +15 -13
- package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
- package/{index-8ggztp8k.js → index-fyq1z6kt.js} +68 -201
- package/{index-1w2qaee2.js → index-g1mbppa9.js} +331 -259
- package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
- package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
- package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/index-mdjc93e1.js +33 -0
- package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
- package/{index-56d2g8ms.js → index-qwa5k1j8.js} +1 -1
- package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
- package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
- package/index-vk0pa0mn.js +240 -0
- package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-vsreyy42.js → localRegistry.command-y176p558.js} +15 -11
- package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
- package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-md2teygq.js → subspace.command-5c7yasd7.js} +424 -364
- package/templates/appSample/lib/task/Task.Util.tsx +7 -7
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
- package/templates/appSample/ui/QuantityControl.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +23 -52
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{tunnel.command-jx3tdm2d.js → tunnel.command-2kw8vt9g.js} +8 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-yxd7rr7g.js → workspace.command-6qfgq4b4.js} +38 -34
- package/TunnelShare-q4jq08qg.js +0 -9
- package/cloud.script-h6pvcpa2.js +0 -20
- package/context.runner-atcrzz5b.js +0 -21
- package/index-6w0rbr9d.js +0 -68
- package/index-n68jx79w.js +0 -120
- package/index-pgj2rt78.js +0 -404
|
@@ -4,916 +4,599 @@ import {
|
|
|
4
4
|
FileSys2
|
|
5
5
|
} from "./index-52w0fvh8.js";
|
|
6
6
|
|
|
7
|
-
// pkgs/@akanjs/devkit/
|
|
7
|
+
// pkgs/@akanjs/devkit/executors.ts
|
|
8
|
+
import {
|
|
9
|
+
exec,
|
|
10
|
+
fork,
|
|
11
|
+
spawn as spawn2
|
|
12
|
+
} from "child_process";
|
|
13
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
14
|
+
import {
|
|
15
|
+
copyFile,
|
|
16
|
+
cp as cpEntry,
|
|
17
|
+
lstat,
|
|
18
|
+
mkdir as mkdir3,
|
|
19
|
+
readdir as readDirEntries,
|
|
20
|
+
realpath,
|
|
21
|
+
rm as rm4,
|
|
22
|
+
stat as stat2,
|
|
23
|
+
symlink
|
|
24
|
+
} from "fs/promises";
|
|
25
|
+
import path13 from "path";
|
|
26
|
+
import { pathToFileURL } from "url";
|
|
27
|
+
import {
|
|
28
|
+
capitalize,
|
|
29
|
+
getPageSourceFileViolation,
|
|
30
|
+
isRouteSourceFile,
|
|
31
|
+
Logger as Logger3,
|
|
32
|
+
parseRouteModuleKey,
|
|
33
|
+
toError as toError2,
|
|
34
|
+
validatePageSourceFile,
|
|
35
|
+
validateSubRoutePageKey
|
|
36
|
+
} from "akanjs/common";
|
|
37
|
+
var {$ } = globalThis.Bun;
|
|
38
|
+
import chalk2 from "chalk";
|
|
39
|
+
|
|
40
|
+
// pkgs/@akanjs/devkit/agentsIndex.ts
|
|
41
|
+
var recipeScannerLoad = null;
|
|
42
|
+
var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
|
|
43
|
+
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
44
|
+
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
45
|
+
var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
46
|
+
var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
|
|
47
|
+
|
|
48
|
+
${block}`;
|
|
49
|
+
var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
50
|
+
var readDevkitVersion = async () => {
|
|
51
|
+
const { readFile } = await import("fs/promises");
|
|
52
|
+
const { getDirname } = await import("./fileSys-wzzrntkw.js");
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
55
|
+
return JSON.parse(raw).version ?? null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var upsertAgentBlock = (existing, block) => {
|
|
61
|
+
const managed = `${AGENT_BLOCK_START}
|
|
62
|
+
${block}
|
|
63
|
+
${AGENT_BLOCK_END}`;
|
|
64
|
+
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
65
|
+
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
66
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
67
|
+
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
68
|
+
}
|
|
69
|
+
return `${existing.replace(/\s*$/, "")}
|
|
70
|
+
|
|
71
|
+
${managed}
|
|
72
|
+
`;
|
|
73
|
+
};
|
|
74
|
+
var extractAgentBlock = (content) => {
|
|
75
|
+
const startIndex = content.indexOf(AGENT_BLOCK_START);
|
|
76
|
+
const endIndex = content.indexOf(AGENT_BLOCK_END);
|
|
77
|
+
if (startIndex < 0 || endIndex <= startIndex)
|
|
78
|
+
return null;
|
|
79
|
+
return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
|
|
80
|
+
};
|
|
81
|
+
var signatureOf = (recipe) => {
|
|
82
|
+
const entries = Object.entries(recipe.variants);
|
|
83
|
+
if (entries.length === 0)
|
|
84
|
+
return "";
|
|
85
|
+
const parts = entries.map(([key, values]) => {
|
|
86
|
+
if (values.length === 1 && values[0] === "true")
|
|
87
|
+
return `${key}?`;
|
|
88
|
+
const def = recipe.defaultVariants?.[key];
|
|
89
|
+
return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
|
|
90
|
+
});
|
|
91
|
+
return `(${parts.join(" \xB7 ")})`;
|
|
92
|
+
};
|
|
93
|
+
var renderRecipeEntries = (recipes) => {
|
|
94
|
+
const groups = new Map;
|
|
95
|
+
for (const recipe of recipes)
|
|
96
|
+
groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
|
|
97
|
+
return [...groups.entries()].map(([importFrom, list]) => {
|
|
98
|
+
const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
|
|
99
|
+
`);
|
|
100
|
+
return `Import from \`${importFrom}\`:
|
|
101
|
+
${items}`;
|
|
102
|
+
}).join(`
|
|
103
|
+
|
|
104
|
+
`);
|
|
105
|
+
};
|
|
106
|
+
var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
|
|
107
|
+
const { collectRecipeSources } = await loadRecipeScanner();
|
|
108
|
+
const sources = [
|
|
109
|
+
...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
|
|
110
|
+
];
|
|
111
|
+
for (const lib of [...new Set(libDeps)].sort()) {
|
|
112
|
+
if (scope.type === "lib" && lib === scope.name)
|
|
113
|
+
continue;
|
|
114
|
+
sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
|
|
115
|
+
}
|
|
116
|
+
return sources;
|
|
117
|
+
};
|
|
118
|
+
var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
|
|
119
|
+
const { scanRecipes } = await loadRecipeScanner();
|
|
120
|
+
return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
|
|
121
|
+
};
|
|
122
|
+
var renderScopeAgentBlock = (scope, recipes) => {
|
|
123
|
+
const scopePath = `${scope.type}s/${scope.name}`;
|
|
124
|
+
const intro = `## Recipes In Scope
|
|
125
|
+
|
|
126
|
+
UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
|
|
127
|
+
\`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
|
|
128
|
+
arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
|
|
129
|
+
boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
|
|
130
|
+
and verified by \`akan lint ${scope.name}\`.`;
|
|
131
|
+
if (recipes.length === 0) {
|
|
132
|
+
return `${intro}
|
|
133
|
+
|
|
134
|
+
No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
|
|
135
|
+
root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
|
|
136
|
+
folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
|
|
137
|
+
}
|
|
138
|
+
return `${intro}
|
|
139
|
+
|
|
140
|
+
${renderRecipeEntries(recipes)}`;
|
|
141
|
+
};
|
|
142
|
+
var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
|
|
143
|
+
|
|
144
|
+
Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
|
|
145
|
+
root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
|
|
146
|
+
section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
|
|
147
|
+
markers freely.
|
|
148
|
+
|
|
149
|
+
${AGENT_BLOCK_START}
|
|
150
|
+
${block}
|
|
151
|
+
${AGENT_BLOCK_END}
|
|
152
|
+
`;
|
|
153
|
+
var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
|
|
154
|
+
|
|
155
|
+
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
156
|
+
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
157
|
+
|
|
158
|
+
Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
|
|
159
|
+
|
|
160
|
+
- Restates the identifier, the signature, or the line under it \u2192 delete it.
|
|
161
|
+
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
|
|
162
|
+
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
|
|
163
|
+
- Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
|
|
164
|
+
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
165
|
+
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
|
|
166
|
+
keep it, one line.
|
|
167
|
+
|
|
168
|
+
That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
169
|
+
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
170
|
+
surrounding code carries none, your diff carries none.`;
|
|
171
|
+
var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
|
|
172
|
+
|
|
173
|
+
@AGENTS.md
|
|
174
|
+
|
|
175
|
+
${CLAUDE_COMMENT_RULE}
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
8
179
|
import path from "path";
|
|
9
|
-
|
|
180
|
+
|
|
181
|
+
// pkgs/@akanjs/devkit/akanConfig/types.ts
|
|
182
|
+
import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
|
|
183
|
+
|
|
184
|
+
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
185
|
+
class AkanBin {
|
|
186
|
+
static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
|
|
187
|
+
static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
188
|
+
static parse(declared, owner, baseDir) {
|
|
189
|
+
if (declared === undefined)
|
|
190
|
+
return {};
|
|
191
|
+
if (!declared || typeof declared !== "object" || Array.isArray(declared))
|
|
192
|
+
throw new Error(`${owner}: bin maps each executable's name to its platforms`);
|
|
193
|
+
return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
|
|
194
|
+
}
|
|
195
|
+
static isArchive(source) {
|
|
196
|
+
return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
|
|
197
|
+
}
|
|
198
|
+
static #platforms(name, platforms, owner, baseDir) {
|
|
199
|
+
if (!AkanBin.fileName.test(name))
|
|
200
|
+
throw new Error(`${owner}: bin.${name} is not a file name`);
|
|
201
|
+
if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
|
|
202
|
+
throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
|
|
203
|
+
const resolved = {};
|
|
204
|
+
for (const [platform, source] of Object.entries(platforms)) {
|
|
205
|
+
if (!binPlatforms.includes(platform))
|
|
206
|
+
throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
|
|
207
|
+
resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
|
|
208
|
+
}
|
|
209
|
+
return resolved;
|
|
210
|
+
}
|
|
211
|
+
static #source(at, source, baseDir) {
|
|
212
|
+
if (!source || typeof source !== "object" || Array.isArray(source))
|
|
213
|
+
throw new Error(`${at} must be { url, sha256 } or { path }`);
|
|
214
|
+
const { url, sha256, path: localPath, file, ...rest } = source;
|
|
215
|
+
const extra = Object.keys(rest);
|
|
216
|
+
if (extra.length)
|
|
217
|
+
throw new Error(`${at}: unknown key ${extra.join(", ")}`);
|
|
218
|
+
if (url === undefined === (localPath === undefined))
|
|
219
|
+
throw new Error(`${at} takes either url and sha256, or path`);
|
|
220
|
+
const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
|
|
221
|
+
const archive = AkanBin.isArchive(resolved);
|
|
222
|
+
if (file === undefined) {
|
|
223
|
+
if (archive)
|
|
224
|
+
throw new Error(`${at} names an archive: file picks the executable inside it`);
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
if (!archive)
|
|
228
|
+
throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
|
|
229
|
+
const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
|
|
230
|
+
if (!parts.length || path.isAbsolute(file) || parts.includes(".."))
|
|
231
|
+
throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
|
|
232
|
+
return { ...resolved, file: parts.join("/") };
|
|
233
|
+
}
|
|
234
|
+
static #urlSource(at, url, sha256) {
|
|
235
|
+
if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
|
|
236
|
+
throw new Error(`${at}: url must be an http(s) address`);
|
|
237
|
+
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
|
|
238
|
+
throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
|
|
239
|
+
return { url, sha256: sha256.toLowerCase() };
|
|
240
|
+
}
|
|
241
|
+
static #pathSource(at, localPath, sha256, baseDir) {
|
|
242
|
+
if (typeof localPath !== "string" || !localPath)
|
|
243
|
+
throw new Error(`${at}: path must name a file`);
|
|
244
|
+
if (sha256 !== undefined)
|
|
245
|
+
throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
|
|
246
|
+
return { path: path.resolve(baseDir, localPath) };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
|
|
250
|
+
import fs from "fs";
|
|
251
|
+
import path3 from "path";
|
|
252
|
+
import { fileURLToPath } from "url";
|
|
253
|
+
import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
|
|
254
|
+
import { resolveAkanI18nConfig, trimSlashes } from "akanjs/common";
|
|
255
|
+
|
|
256
|
+
// pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
|
|
257
|
+
import { existsSync, realpathSync } from "fs";
|
|
258
|
+
import path2 from "path";
|
|
259
|
+
import { Logger, toError } from "akanjs/common";
|
|
10
260
|
|
|
11
261
|
class ServerEnvProfile {
|
|
12
262
|
static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
|
|
263
|
+
static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
|
|
13
264
|
static fileName(environment, profile) {
|
|
14
265
|
return `env.server.${environment}.${profile}.ts`;
|
|
15
266
|
}
|
|
16
267
|
static profileFileOf(file, profile) {
|
|
17
268
|
return file.replace(/\.ts$/, `.${profile}.ts`);
|
|
18
269
|
}
|
|
19
|
-
static
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const result = ServerEnvProfile.strip(child, at ? `${at}.${key}` : key, underSecret || secretKey.test(key));
|
|
27
|
-
stripped.push(...result.stripped);
|
|
28
|
-
return result.value;
|
|
29
|
-
};
|
|
30
|
-
if (Array.isArray(value))
|
|
31
|
-
return { value: value.map((child, idx) => strip(String(idx), child)), stripped };
|
|
32
|
-
const kept = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, strip(key, child)]).filter(([, child]) => child !== undefined));
|
|
33
|
-
return { value: kept, stripped };
|
|
34
|
-
}
|
|
35
|
-
static secretValues(value, at = "", underSecret = false) {
|
|
36
|
-
if (typeof value === "string") {
|
|
37
|
-
const named = secretKey.test(at.split(".").at(-1) ?? "");
|
|
38
|
-
const distinct = value.length >= 16 && !/^https?:\/\//.test(value) && !/^[a-z]+(?:[._-][a-z]+)*$/.test(value);
|
|
39
|
-
return underSecret && value.length >= 8 && (named || distinct) ? [{ path: at, value }] : [];
|
|
270
|
+
static #url(value) {
|
|
271
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
|
|
272
|
+
return null;
|
|
273
|
+
try {
|
|
274
|
+
return new URL(value);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
40
277
|
}
|
|
41
|
-
if (!value || typeof value !== "object")
|
|
42
|
-
return [];
|
|
43
|
-
return Object.entries(value).flatMap(([key, child]) => ServerEnvProfile.secretValues(child, at ? `${at}.${key}` : key, underSecret || secretKey.test(key)));
|
|
44
278
|
}
|
|
45
|
-
static
|
|
279
|
+
static #carriesCredential(url) {
|
|
280
|
+
return !!url.password || [...url.searchParams.keys()].some((name) => Logger.isSecretKey(name));
|
|
281
|
+
}
|
|
282
|
+
static #isCredentialUrl(value) {
|
|
283
|
+
const url = ServerEnvProfile.#url(value);
|
|
284
|
+
return !!url && ServerEnvProfile.#carriesCredential(url);
|
|
285
|
+
}
|
|
286
|
+
static #isSecret(value, underSecret) {
|
|
287
|
+
return underSecret && !!value.trim() || ServerEnvProfile.#isCredentialUrl(value);
|
|
288
|
+
}
|
|
289
|
+
static #fold(value, fold, at = "", underSecret = false) {
|
|
46
290
|
if (typeof value === "string")
|
|
47
|
-
return
|
|
291
|
+
return fold.text(value, at, underSecret);
|
|
48
292
|
if (!value || typeof value !== "object")
|
|
49
|
-
return
|
|
50
|
-
return Object.entries(value).
|
|
293
|
+
return fold.other(value);
|
|
294
|
+
return fold.node(value, Object.entries(value).map(([key, child]) => [
|
|
295
|
+
key,
|
|
296
|
+
ServerEnvProfile.#fold(child, fold, at ? `${at}.${key}` : key, underSecret || Logger.isSecretKey(key))
|
|
297
|
+
]));
|
|
298
|
+
}
|
|
299
|
+
static #collect(text) {
|
|
300
|
+
return { text, other: () => [], node: (_, children) => children.flatMap(([, found]) => found) };
|
|
301
|
+
}
|
|
302
|
+
static strip(value) {
|
|
303
|
+
return ServerEnvProfile.#fold(value, {
|
|
304
|
+
text: (text, at, underSecret) => ServerEnvProfile.#isSecret(text, underSecret) ? { value: undefined, stripped: [at] } : { value: text, stripped: [] },
|
|
305
|
+
other: (other) => ({ value: other, stripped: [] }),
|
|
306
|
+
node: (node, children) => {
|
|
307
|
+
const stripped = children.flatMap(([, result]) => result.stripped);
|
|
308
|
+
if (Array.isArray(node))
|
|
309
|
+
return {
|
|
310
|
+
value: children.flatMap(([, result]) => result.value === undefined || result.stripped.length && typeof result.value === "object" && !Object.keys(result.value ?? {}).length ? [] : [result.value]),
|
|
311
|
+
stripped
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
value: Object.fromEntries(children.flatMap(([key, result]) => result.value === undefined ? [] : [[key, result.value]])),
|
|
315
|
+
stripped
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
static secretValues(value) {
|
|
321
|
+
return ServerEnvProfile.#fold(value, ServerEnvProfile.#collect((text, at, underSecret) => {
|
|
322
|
+
const url = ServerEnvProfile.#url(text);
|
|
323
|
+
if (url)
|
|
324
|
+
return ServerEnvProfile.#carriesCredential(url) || underSecret && (/webhook/i.test(at) || ServerEnvProfile.distinctive(text)) ? [...new Set([text, url.password])].filter((part) => part.length >= 8).map((part) => ({ path: at, value: part })) : [];
|
|
325
|
+
const named = Logger.isSecretKey(at.split(".").at(-1) ?? "");
|
|
326
|
+
return underSecret && text.length >= 8 && (named || ServerEnvProfile.distinctive(text)) ? [{ path: at, value: text }] : [];
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
static distinctive(value) {
|
|
330
|
+
const url = ServerEnvProfile.#url(value);
|
|
331
|
+
if (url)
|
|
332
|
+
return ServerEnvProfile.#carriesCredential(url) || [...url.pathname.split("/"), ...url.searchParams.values()].some((part) => ServerEnvProfile.distinctive(part));
|
|
333
|
+
return value.length >= 16 && !/^https?:\/\//.test(value) && !/^[a-z]+(?:[._-][a-z]+)*$/.test(value);
|
|
334
|
+
}
|
|
335
|
+
static secretPaths(value) {
|
|
336
|
+
return ServerEnvProfile.#fold(value, ServerEnvProfile.#collect((text, at, underSecret) => ServerEnvProfile.#isSecret(text, underSecret) ? [at] : []));
|
|
51
337
|
}
|
|
52
|
-
static async evaluate(file, cwd) {
|
|
338
|
+
static async evaluate(file, cwd, load) {
|
|
339
|
+
return (await ServerEnvProfile.#evaluated(file, cwd, load)).value;
|
|
340
|
+
}
|
|
341
|
+
static async#evaluated(file, cwd, load) {
|
|
342
|
+
const pattern = (regex, flags = "") => `new RegExp(${JSON.stringify(regex.source)}, "${flags}")`;
|
|
343
|
+
const literal = /(["'`])((?:\\[\s\S]|(?!\1)[^\\])*)\1/;
|
|
344
|
+
const frame = /^\s*at (?:[^(]*\()?(.+):(\d+):(\d+)\)?\s*$/;
|
|
53
345
|
const script = [
|
|
54
|
-
"const
|
|
55
|
-
'
|
|
56
|
-
"
|
|
346
|
+
"const waiting = new Map();",
|
|
347
|
+
'process.on("message", ({ load, error, loader, contents }) => {',
|
|
348
|
+
" const reply = waiting.get(load);",
|
|
349
|
+
" waiting.delete(load);",
|
|
350
|
+
" if (error === undefined) reply?.resolve({ loader, contents });",
|
|
351
|
+
" else reply?.reject(new Error(error));",
|
|
352
|
+
"});",
|
|
353
|
+
"if (process.env.AKAN_PROFILE_ENV_LOAD)",
|
|
354
|
+
" Bun.plugin({",
|
|
355
|
+
' name: "akan-server-env",',
|
|
356
|
+
" setup(build) {",
|
|
357
|
+
" build.onLoad({ filter: new RegExp(process.env.AKAN_PROFILE_ENV_LOAD) }, (args) =>",
|
|
358
|
+
" new Promise((resolve, reject) => {",
|
|
359
|
+
" waiting.set(args.path, { resolve, reject });",
|
|
360
|
+
" process.send({ load: args.path });",
|
|
361
|
+
" }));",
|
|
362
|
+
" },",
|
|
363
|
+
" });",
|
|
364
|
+
`const literal = ${pattern(literal, "g")};`,
|
|
365
|
+
`const interpolation = ${pattern(/\$\{[^}]*\}/)};`,
|
|
366
|
+
`const frameOf = ${pattern(frame)};`,
|
|
367
|
+
`const evaluating = ${pattern(/\s*\(evaluating '[\s\S]*'\)\s*$/)};`,
|
|
368
|
+
"const literalsOf = async (file) =>",
|
|
369
|
+
' [...(await Bun.file(file).text().catch(() => "")).matchAll(literal)].flatMap((match) => match[2].split(interpolation));',
|
|
370
|
+
"const describe = async (error) => {",
|
|
371
|
+
" const first = Array.isArray(error?.errors) && error.errors.length ? error.errors[0] : error;",
|
|
372
|
+
' const frame = String(first?.stack ?? "").split("\\n").map((line) => frameOf.exec(line)).find(Boolean);',
|
|
373
|
+
" const at = first?.position?.file ? [first.position.file, first.position.line, first.position.column] : frame ? frame.slice(1, 4) : [];",
|
|
374
|
+
" const files = [process.env.AKAN_PROFILE_ENV_FILE, at[0]].filter(Boolean);",
|
|
375
|
+
" const literals = (await Promise.all(files.map(literalsOf))).flat().filter((part) => part.length >= 4);",
|
|
376
|
+
' let message = String(first?.message ?? first).replace(evaluating, "");',
|
|
377
|
+
" for (const part of literals.sort((a, b) => b.length - a.length))",
|
|
378
|
+
' message = message.split(part).join("[redacted]");',
|
|
379
|
+
' const name = typeof first?.name === "string" && /^[A-Za-z]+$/.test(first.name) ? first.name : "Error";',
|
|
380
|
+
' return name + ": " + message.slice(0, 200) + (at.length ? " (at " + at.join(":") + ")" : "");',
|
|
381
|
+
"};",
|
|
382
|
+
"try {",
|
|
383
|
+
" const { env } = await import(process.env.AKAN_PROFILE_ENV_FILE);",
|
|
384
|
+
" const lossy = [];",
|
|
385
|
+
" const walk = (value, at) => {",
|
|
386
|
+
' if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint" || (typeof value === "number" && !Number.isFinite(value))) return lossy.push(at);',
|
|
387
|
+
' if (!value || typeof value !== "object") return;',
|
|
388
|
+
" const proto = Object.getPrototypeOf(value);",
|
|
389
|
+
" if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return lossy.push(at);",
|
|
390
|
+
' for (const [key, child] of Object.entries(value)) walk(child, at ? at + "." + key : key);',
|
|
391
|
+
" };",
|
|
392
|
+
' walk(env, "");',
|
|
393
|
+
' const plain = (_, value) => (typeof value === "function" || typeof value === "symbol" ? undefined : typeof value === "bigint" ? String(value) : value);',
|
|
394
|
+
' process.send({ env: JSON.stringify(env ?? null, plain) ?? "null", lossy }, undefined, undefined, () => process.disconnect());',
|
|
395
|
+
"} catch (error) {",
|
|
396
|
+
" process.exitCode = 1;",
|
|
397
|
+
" process.send({ failure: await describe(error) }, undefined, undefined, () => process.disconnect());",
|
|
398
|
+
"}"
|
|
57
399
|
].join(`
|
|
58
400
|
`);
|
|
401
|
+
let sent;
|
|
402
|
+
let failure;
|
|
403
|
+
let loadError;
|
|
59
404
|
const child = Bun.spawn([process.execPath, "--eval", script], {
|
|
60
405
|
cwd,
|
|
61
|
-
env: {
|
|
62
|
-
|
|
63
|
-
|
|
406
|
+
env: {
|
|
407
|
+
PATH: process.env.PATH ?? "",
|
|
408
|
+
HOME: process.env.HOME ?? "",
|
|
409
|
+
AKAN_PROFILE_ENV_FILE: file,
|
|
410
|
+
...load ? { AKAN_PROFILE_ENV_LOAD: ServerEnvProfile.envFilePattern.source } : {}
|
|
411
|
+
},
|
|
412
|
+
stdout: "ignore",
|
|
413
|
+
stderr: "ignore",
|
|
414
|
+
serialization: "json",
|
|
415
|
+
ipc: (message, subprocess) => {
|
|
416
|
+
if ("env" in message) {
|
|
417
|
+
sent = message;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if ("failure" in message) {
|
|
421
|
+
failure = message.failure;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!load)
|
|
425
|
+
return;
|
|
426
|
+
const requested = message.load;
|
|
427
|
+
load(requested).then(({ loader, contents }) => ({ load: requested, loader, contents }), (error) => {
|
|
428
|
+
loadError ??= error;
|
|
429
|
+
return { load: requested, error: toError(error).message };
|
|
430
|
+
}).then((reply) => subprocess.send(reply)).catch(() => {
|
|
431
|
+
return;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
64
434
|
});
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
435
|
+
const exitCode = await child.exited;
|
|
436
|
+
if (exitCode !== 0 || !sent) {
|
|
437
|
+
if (loadError)
|
|
438
|
+
throw loadError;
|
|
439
|
+
throw new Error(`${path2.basename(file)} could not be evaluated to check it for secrets: ${failure ?? `its process exited with code ${exitCode} and sent nothing`}`);
|
|
440
|
+
}
|
|
441
|
+
return { value: JSON.parse(sent.env), lossy: sent.lossy };
|
|
442
|
+
}
|
|
443
|
+
static async sourceInPlaceOf(profileFile, replacedFile, profile) {
|
|
444
|
+
const contents = await Bun.file(profileFile).text();
|
|
445
|
+
const dir = path2.dirname(profileFile);
|
|
446
|
+
const replaced = ServerEnvProfile.#real(replacedFile);
|
|
447
|
+
const selfImports = new Bun.Transpiler({ loader: "ts" }).scanImports(contents).flatMap(({ path: specifier }) => {
|
|
448
|
+
try {
|
|
449
|
+
return ServerEnvProfile.#real(Bun.resolveSync(specifier, dir)) === replaced ? [specifier] : [];
|
|
450
|
+
} catch {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
if (selfImports.length)
|
|
455
|
+
throw new Error(`${path2.basename(profileFile)} imports ${selfImports.join(", ")}, the file a "${profile}" build bundles it in place of, so it would import itself. Write the values it needs into it instead.`);
|
|
456
|
+
return contents;
|
|
457
|
+
}
|
|
458
|
+
static #real(file) {
|
|
459
|
+
return existsSync(file) ? realpathSync.native(file) : path2.resolve(file);
|
|
460
|
+
}
|
|
461
|
+
static async libEnvSource(file, profile, chain = []) {
|
|
462
|
+
if (chain.includes(file))
|
|
463
|
+
throw new Error(`${[...chain, file].join(" -> ")}: lib env files reach each other, which a "${profile}" build cannot swap.`);
|
|
464
|
+
const profileFile = ServerEnvProfile.profileFileOf(file, profile);
|
|
465
|
+
const libRoot = path2.dirname(path2.dirname(file));
|
|
466
|
+
if (await Bun.file(profileFile).exists()) {
|
|
467
|
+
const contents = await ServerEnvProfile.sourceInPlaceOf(profileFile, file, profile);
|
|
468
|
+
const reached = [...chain, file];
|
|
469
|
+
const value = await ServerEnvProfile.evaluate(profileFile, libRoot, (envFile) => ServerEnvProfile.#underProfile(envFile, profile, new Set, reached));
|
|
470
|
+
const secrets = ServerEnvProfile.secretPaths(value);
|
|
471
|
+
if (secrets.length)
|
|
472
|
+
throw new Error(`${profileFile} holds secret-named values, which the "${profile}" bundle carries to every computer it runs on: ${secrets.join(", ")}.`);
|
|
473
|
+
return { loader: "ts", contents, profileFile: true, stripped: [] };
|
|
474
|
+
}
|
|
475
|
+
const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(file).text());
|
|
476
|
+
if (exports.some((name) => name !== "env"))
|
|
477
|
+
throw new Error(`${file} exports ${exports.join(", ")}: a "${profile}" build carries only a lib env's \`env\`, so give the lib ${path2.basename(profileFile)}.`);
|
|
478
|
+
const evaluated = await ServerEnvProfile.#evaluated(file, libRoot);
|
|
479
|
+
if (evaluated.lossy.length)
|
|
480
|
+
throw new Error(`${file} holds ${evaluated.lossy.join(", ")}, which a "${profile}" build cannot carry: it bundles a lib env's defaults as JSON, which keeps no function, Map, Set, Date or class instance. Give the lib ${path2.basename(profileFile)}.`);
|
|
481
|
+
const { value, stripped } = ServerEnvProfile.strip(evaluated.value);
|
|
482
|
+
return {
|
|
483
|
+
loader: "js",
|
|
484
|
+
contents: `export const env = ${JSON.stringify(value ?? {})};
|
|
485
|
+
`,
|
|
486
|
+
profileFile: false,
|
|
487
|
+
stripped
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
static async#underProfile(envFile, profile, appEnvDirs, chain) {
|
|
491
|
+
const name = path2.basename(envFile, ".ts").slice("env.server.".length);
|
|
492
|
+
if (appEnvDirs.has(path2.dirname(envFile)) || name === "type" || name === "example")
|
|
493
|
+
return { loader: "ts", contents: await Bun.file(envFile).text() };
|
|
494
|
+
const { loader, contents } = await ServerEnvProfile.libEnvSource(envFile, profile, chain);
|
|
495
|
+
return { loader, contents };
|
|
74
496
|
}
|
|
75
497
|
static async assertShippable({ envDir, environment, profile }) {
|
|
76
498
|
if (!ServerEnvProfile.namePattern.test(profile))
|
|
77
499
|
throw new Error(`The server env profile "${profile}" must be a lowercase-first name of letters, digits and dashes.`);
|
|
78
500
|
const name = ServerEnvProfile.fileName(environment, profile);
|
|
79
|
-
const file =
|
|
501
|
+
const file = path2.join(envDir, name);
|
|
80
502
|
if (!await Bun.file(file).exists())
|
|
81
503
|
throw new Error(`The server env profile "${profile}" bundles env/${name} in place of env/env.server.${environment}.ts, and there is no such file.`);
|
|
82
|
-
|
|
504
|
+
await ServerEnvProfile.sourceInPlaceOf(file, path2.join(envDir, `env.server.${environment}.ts`), profile);
|
|
505
|
+
const appEnvDirs = new Set([path2.resolve(envDir), realpathSync.native(envDir)]);
|
|
506
|
+
const value = await ServerEnvProfile.evaluate(file, path2.dirname(envDir), (envFile) => ServerEnvProfile.#underProfile(envFile, profile, appEnvDirs, []));
|
|
507
|
+
const secrets = ServerEnvProfile.secretPaths(value);
|
|
83
508
|
if (secrets.length)
|
|
84
509
|
throw new Error(`env/${name} holds secret-named values, which a profile's bundle carries to every computer it runs on: ${secrets.join(", ")}. Leave them out, or read them from the process env of the server that needs them.`);
|
|
85
510
|
}
|
|
86
511
|
}
|
|
87
512
|
|
|
88
|
-
// pkgs/@akanjs/devkit/
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return true;
|
|
141
|
-
if (ts.isCallExpression(statement.expression))
|
|
142
|
-
return true;
|
|
143
|
-
if (ts.isIdentifier(statement.expression))
|
|
144
|
-
defaultIdentifier = statement.expression.text;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
148
|
-
for (const specifier of statement.exportClause.elements) {
|
|
149
|
-
if (specifier.name.text !== "default")
|
|
150
|
-
continue;
|
|
151
|
-
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
156
|
-
}
|
|
157
|
-
#hasModifier(node, kind) {
|
|
158
|
-
const ts = this.#ts;
|
|
159
|
-
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
160
|
-
}
|
|
161
|
-
#isAsyncFunctionExpression(node) {
|
|
162
|
-
const ts = this.#ts;
|
|
163
|
-
return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
|
|
513
|
+
// pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
|
|
514
|
+
var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
515
|
+
var DEFAULT_OPTIMIZE_IMPORTS = [
|
|
516
|
+
"lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
|
|
517
|
+
"@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
|
|
518
|
+
"@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
|
|
519
|
+
"@material-ui/icons @tabler/icons-react mui-core react-icons/*"
|
|
520
|
+
].flatMap((line) => line.split(" "));
|
|
521
|
+
var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
|
|
522
|
+
var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
523
|
+
var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
|
|
524
|
+
var DEFAULT_AKAN_IMAGE_CONFIG = {
|
|
525
|
+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
526
|
+
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
527
|
+
formats: ["image/webp"],
|
|
528
|
+
qualities: [75],
|
|
529
|
+
minimumCacheTTL: 14400,
|
|
530
|
+
remotePatterns: [],
|
|
531
|
+
localPatterns: [{ pathname: "/**" }],
|
|
532
|
+
dangerouslyAllowSVG: false,
|
|
533
|
+
maximumRedirects: 3,
|
|
534
|
+
fetchTimeoutMs: 7000,
|
|
535
|
+
maxRemoteBytes: 25 * 1024 * 1024,
|
|
536
|
+
maxConcurrency: 0
|
|
537
|
+
};
|
|
538
|
+
var normalizeIndexPath = (indexPath) => {
|
|
539
|
+
const normalized = indexPath?.trim();
|
|
540
|
+
if (!normalized)
|
|
541
|
+
return;
|
|
542
|
+
const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
543
|
+
return path === "/" ? "/" : path;
|
|
544
|
+
};
|
|
545
|
+
var normalizeStringList = (values) => {
|
|
546
|
+
const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
547
|
+
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
|
548
|
+
};
|
|
549
|
+
var sanitizeAppIdSegment = (value) => {
|
|
550
|
+
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
551
|
+
if (!cleaned)
|
|
552
|
+
return "app";
|
|
553
|
+
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
|
|
554
|
+
};
|
|
555
|
+
var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
|
|
556
|
+
var normalizeDeepLinkDomain = (domain) => {
|
|
557
|
+
const normalized = domain.trim();
|
|
558
|
+
if (!normalized)
|
|
559
|
+
return "";
|
|
560
|
+
try {
|
|
561
|
+
const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
|
|
562
|
+
return url.host.toLowerCase();
|
|
563
|
+
} catch {
|
|
564
|
+
return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
|
|
164
565
|
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
|
|
168
|
-
var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
|
|
169
|
-
var IMPLICIT_LAYOUT_DIR = path3.join(".akan", "generated", "root-layouts");
|
|
170
|
-
var IMPLICIT_DICT_DIR = path3.join(".akan", "generated", "dict");
|
|
171
|
-
var IMPLICIT_OVERRIDES_DIR = path3.join(".akan", "generated", "overrides");
|
|
172
|
-
var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
|
|
173
|
-
var importSpecifier = (fromAbsPath, toAbsPath) => {
|
|
174
|
-
const rel = path3.relative(path3.dirname(fromAbsPath), toAbsPath).split(path3.sep).join("/");
|
|
175
|
-
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
176
566
|
};
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const keep = new Set(keepAbsPaths);
|
|
194
|
-
const names = await readdir(dirAbsPath).catch(() => []);
|
|
195
|
-
await Promise.all(names.map((name) => path3.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm(absPath, { force: true })));
|
|
196
|
-
}
|
|
197
|
-
function getRootBoundarySegments(key) {
|
|
198
|
-
const match = LAYOUT_KEY_RE.exec(key);
|
|
199
|
-
if (!match)
|
|
200
|
-
return null;
|
|
201
|
-
const prefix = match[1]?.replace(/\/$/, "");
|
|
202
|
-
if (!prefix)
|
|
567
|
+
var normalizeDeepLinks = (deepLinks) => {
|
|
568
|
+
const schemes = normalizeStringList(deepLinks?.schemes);
|
|
569
|
+
const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
|
|
570
|
+
if (!schemes && !domains)
|
|
571
|
+
return;
|
|
572
|
+
return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
|
|
573
|
+
};
|
|
574
|
+
var emptyLibContributions = () => ({
|
|
575
|
+
externalLibs: [],
|
|
576
|
+
trustedDependencies: [],
|
|
577
|
+
bin: [],
|
|
578
|
+
docker: { preRuns: [], postRuns: [] },
|
|
579
|
+
keepFonts: []
|
|
580
|
+
});
|
|
581
|
+
var normalizePackageNames = (names, owner) => {
|
|
582
|
+
if (names === undefined)
|
|
203
583
|
return [];
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const visible = segments.slice(firstVisibleIndex);
|
|
220
|
-
const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
|
|
221
|
-
return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
|
|
222
|
-
}
|
|
223
|
-
function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
|
|
224
|
-
const boundaries = new Map;
|
|
225
|
-
for (const key of pageKeys) {
|
|
226
|
-
const segments = getRootBoundarySegments(key);
|
|
227
|
-
if (!segments)
|
|
228
|
-
continue;
|
|
229
|
-
if (!isRootBoundarySegments(segments, basePaths))
|
|
230
|
-
continue;
|
|
231
|
-
const id = segments.join("/");
|
|
232
|
-
boundaries.set(id, {
|
|
233
|
-
sourceKey: key,
|
|
234
|
-
sourceAbsPath: path3.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
|
|
235
|
-
segments
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
if (boundaries.size === 0)
|
|
239
|
-
boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
|
|
240
|
-
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
584
|
+
if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
|
|
585
|
+
throw new Error(`${owner}: trustedDependencies lists package names`);
|
|
586
|
+
return [...new Set(names.map((name) => name.trim()))];
|
|
587
|
+
};
|
|
588
|
+
var normalizeKeepFonts = (keepFonts) => [
|
|
589
|
+
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
|
|
590
|
+
];
|
|
591
|
+
var dedupeDockerRuns = (runs) => {
|
|
592
|
+
const byKey = new Map;
|
|
593
|
+
for (const run of runs)
|
|
594
|
+
byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
595
|
+
return [...byKey.values()];
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
class LibConfigRefusal extends Error {
|
|
241
599
|
}
|
|
242
|
-
function hasAncestorRootBoundary(boundary, boundaries) {
|
|
243
|
-
return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
|
|
244
|
-
}
|
|
245
|
-
function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
|
|
246
|
-
const rootLayoutKey = pageKeys.find((key) => {
|
|
247
|
-
const segments = getRootBoundarySegments(key);
|
|
248
|
-
return segments !== null && segments.length === 0;
|
|
249
|
-
});
|
|
250
|
-
return rootLayoutKey ? path3.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
|
|
251
|
-
}
|
|
252
|
-
async function assertEnvClientConvention(appCwdPath, appName) {
|
|
253
|
-
const envPath = path3.join(appCwdPath, "env", "env.client.ts");
|
|
254
|
-
if (!await Bun.file(envPath).exists()) {
|
|
255
|
-
throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
|
|
259
|
-
const absPath = path3.join(path3.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
260
|
-
await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
|
|
261
|
-
|
|
262
|
-
export const allDictionary = getAllDictionary();
|
|
263
|
-
`);
|
|
264
|
-
return absPath;
|
|
265
|
-
}
|
|
266
|
-
async function writeGeneratedRootLayoutFile(opts) {
|
|
267
|
-
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
268
|
-
const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
|
|
269
|
-
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
270
|
-
const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
|
|
271
|
-
const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
|
|
272
|
-
const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
|
|
273
|
-
const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
|
|
274
|
-
const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
|
|
275
|
-
void st;
|
|
276
|
-
` : `import "@apps/${opts.appName}/client";
|
|
277
|
-
`;
|
|
278
|
-
const inheritedLabel = inheritedSourceAbsPath ? path3.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
|
|
279
|
-
const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
|
|
280
|
-
const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
|
|
281
|
-
` : `const inheritedLayout: LayoutModule = {};
|
|
282
|
-
`;
|
|
283
|
-
const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
|
|
284
|
-
const userLabel = opts.boundary.sourceAbsPath ? path3.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
|
|
285
|
-
const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
|
|
286
|
-
const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
|
|
287
|
-
const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
|
|
288
|
-
` : `const UserLayout = ({ children }: LayoutProps) => children;
|
|
289
|
-
const userLayout: LayoutModule = {};
|
|
290
|
-
`;
|
|
291
|
-
const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
|
|
292
|
-
const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
|
|
293
|
-
const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
|
|
294
|
-
const layoutBinding = isAsyncUserLayout ? ` const layout =
|
|
295
|
-
process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
|
|
296
|
-
? await UserLayout({ params, searchParams, children })
|
|
297
|
-
: ${userLayoutElement};
|
|
298
|
-
` : "";
|
|
299
|
-
const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
|
|
300
|
-
const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
|
|
301
|
-
const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
|
|
302
|
-
const layoutExports = `export async function generateHead(props: PageProps) {
|
|
303
|
-
const heads = await Promise.all(
|
|
304
|
-
${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
|
|
305
|
-
);
|
|
306
|
-
return RouteHead.mergeNodes(heads);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
310
|
-
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
311
|
-
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
312
|
-
|
|
313
|
-
${layoutSignature}({ children, params, searchParams }: LayoutProps) {
|
|
314
|
-
${layoutBinding} return `;
|
|
315
|
-
const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
316
|
-
import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
|
|
317
|
-
import type { ReactNode } from "react";
|
|
318
|
-
import { System } from "akanjs/ui";
|
|
319
|
-
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
320
|
-
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
321
|
-
${clientImport}${inheritedImport}${userImport}
|
|
322
|
-
const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
|
|
323
|
-
const defaultFonts = userFonts.filter((font) => font.default);
|
|
324
|
-
if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
|
|
325
|
-
const defaultFont = defaultFonts[0];
|
|
326
|
-
const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
|
|
327
|
-
export const theme = userLayout.theme ?? inheritedLayout.theme;
|
|
328
|
-
export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
|
|
329
|
-
export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
|
|
330
|
-
|
|
331
|
-
${layoutExports}(
|
|
332
|
-
<System.Provider
|
|
333
|
-
of={GeneratedLayout as never}
|
|
334
|
-
appName=${JSON.stringify(opts.appName)}
|
|
335
|
-
${prefix ? `prefix=${JSON.stringify(prefix)}
|
|
336
|
-
` : ""}params={params}
|
|
337
|
-
manifest={manifest}
|
|
338
|
-
env={env}
|
|
339
|
-
theme={theme}
|
|
340
|
-
viewport={viewport}
|
|
341
|
-
fonts={loadFonts(userFonts)}
|
|
342
|
-
className={defaultFontClassName}
|
|
343
|
-
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
344
|
-
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
345
|
-
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
346
|
-
allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
|
|
347
|
-
>
|
|
348
|
-
${layoutChild}
|
|
349
|
-
</System.Provider>
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
353
|
-
import { resolveRouteModule, RouteHead } from "akanjs/client";
|
|
354
|
-
import type { ReactNode } from "react";
|
|
355
|
-
${inheritedImport}${userImport}
|
|
356
|
-
${layoutExports}${layoutReturn};
|
|
357
|
-
}
|
|
358
|
-
`;
|
|
359
|
-
await Bun.write(absPath, source);
|
|
360
|
-
return absPath;
|
|
361
|
-
}
|
|
362
|
-
async function resolveSsrPageEntries(opts) {
|
|
363
|
-
const absPageDir = path3.resolve(opts.appCwdPath, "page");
|
|
364
|
-
const hasSt = await Bun.file(path3.join(opts.appCwdPath, "lib", "st.ts")).exists();
|
|
365
|
-
const basePaths = opts.basePaths ?? [];
|
|
366
|
-
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
367
|
-
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
368
|
-
const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
|
|
369
|
-
const segments = getRootBoundarySegments(key);
|
|
370
|
-
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
371
|
-
}));
|
|
372
|
-
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
373
|
-
const userAbsPath = path3.resolve(absPageDir, key);
|
|
374
|
-
if (OVERRIDES_KEY_RE.test(key)) {
|
|
375
|
-
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
376
|
-
appCwdPath: opts.appCwdPath,
|
|
377
|
-
key,
|
|
378
|
-
userAbsPath
|
|
379
|
-
});
|
|
380
|
-
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
381
|
-
}
|
|
382
|
-
return { key, moduleAbsPath: userAbsPath };
|
|
383
|
-
}));
|
|
384
|
-
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
385
|
-
key: implicitRootLayoutKey(boundary.segments),
|
|
386
|
-
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
387
|
-
appCwdPath: opts.appCwdPath,
|
|
388
|
-
appName: opts.appName,
|
|
389
|
-
boundary,
|
|
390
|
-
rootSourceAbsPath,
|
|
391
|
-
includeStInit: hasSt && boundary.segments.length === 0,
|
|
392
|
-
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
|
|
393
|
-
}),
|
|
394
|
-
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
|
|
395
|
-
})));
|
|
396
|
-
const appCwdAbsPath = path3.resolve(opts.appCwdPath);
|
|
397
|
-
await Promise.all([
|
|
398
|
-
pruneGeneratedFiles(path3.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
|
|
399
|
-
pruneGeneratedFiles(path3.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
|
|
400
|
-
]);
|
|
401
|
-
return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
|
|
402
|
-
}
|
|
403
|
-
async function resolveSsrPageEntriesForApp(app, pageKeys) {
|
|
404
|
-
const config = await app.getConfig();
|
|
405
|
-
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// pkgs/@akanjs/devkit/codegenLock.ts
|
|
409
|
-
import { mkdir, open, readFile, rm as rm2, stat } from "fs/promises";
|
|
410
|
-
import path4 from "path";
|
|
411
|
-
import { Logger } from "akanjs/common";
|
|
412
|
-
|
|
413
|
-
class CodegenLock {
|
|
414
|
-
static fileName = "codegen.lock";
|
|
415
|
-
static waitTimeoutMs = 1e4;
|
|
416
|
-
static unknownHolderStaleMs = 60000;
|
|
417
|
-
static #pollMs = 25;
|
|
418
|
-
static #logger = new Logger("CodegenLock");
|
|
419
|
-
static #queue = Promise.resolve();
|
|
420
|
-
static pathIn(workspaceRoot) {
|
|
421
|
-
return path4.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
|
|
422
|
-
}
|
|
423
|
-
static async run(workspaceRoot, label, fn) {
|
|
424
|
-
const ahead = CodegenLock.#queue;
|
|
425
|
-
let done;
|
|
426
|
-
CodegenLock.#queue = new Promise((resolve) => {
|
|
427
|
-
done = resolve;
|
|
428
|
-
});
|
|
429
|
-
try {
|
|
430
|
-
await CodegenLock.#waitForQueue(ahead, label);
|
|
431
|
-
return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
|
|
432
|
-
} finally {
|
|
433
|
-
done();
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
static async#waitForQueue(ahead, label) {
|
|
437
|
-
let timer = null;
|
|
438
|
-
const expired = new Promise((resolve) => {
|
|
439
|
-
timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
|
|
440
|
-
});
|
|
441
|
-
try {
|
|
442
|
-
if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
|
|
443
|
-
CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
|
|
444
|
-
} finally {
|
|
445
|
-
if (timer)
|
|
446
|
-
clearTimeout(timer);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
static async#withFileLock(workspaceRoot, label, fn) {
|
|
450
|
-
const lockPath = CodegenLock.pathIn(workspaceRoot);
|
|
451
|
-
await mkdir(path4.dirname(lockPath), { recursive: true }).catch(() => {
|
|
452
|
-
return;
|
|
453
|
-
});
|
|
454
|
-
const held = await CodegenLock.#acquire(lockPath, label);
|
|
455
|
-
try {
|
|
456
|
-
return await fn();
|
|
457
|
-
} finally {
|
|
458
|
-
if (held)
|
|
459
|
-
await rm2(lockPath, { force: true }).catch(() => {
|
|
460
|
-
return;
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
static async#acquire(lockPath, label) {
|
|
465
|
-
const deadline = Date.now() + CodegenLock.waitTimeoutMs;
|
|
466
|
-
for (;; ) {
|
|
467
|
-
const handle = await open(lockPath, "wx").catch(() => null);
|
|
468
|
-
if (handle) {
|
|
469
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
|
|
470
|
-
return;
|
|
471
|
-
});
|
|
472
|
-
await handle.close().catch(() => {
|
|
473
|
-
return;
|
|
474
|
-
});
|
|
475
|
-
return true;
|
|
476
|
-
}
|
|
477
|
-
if (await CodegenLock.#reclaimIfAbandoned(lockPath))
|
|
478
|
-
continue;
|
|
479
|
-
if (Date.now() >= deadline) {
|
|
480
|
-
CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
await Bun.sleep(CodegenLock.#pollMs);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
static async#reclaimIfAbandoned(lockPath) {
|
|
487
|
-
const info = await stat(lockPath).catch(() => null);
|
|
488
|
-
if (!info)
|
|
489
|
-
return true;
|
|
490
|
-
const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
|
|
491
|
-
if (holder) {
|
|
492
|
-
if (CodegenLock.#isAlive(holder.pid))
|
|
493
|
-
return false;
|
|
494
|
-
} else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
|
|
495
|
-
return false;
|
|
496
|
-
await rm2(lockPath, { force: true }).catch(() => {
|
|
497
|
-
return;
|
|
498
|
-
});
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
static #parseHolder(raw) {
|
|
502
|
-
try {
|
|
503
|
-
const parsed = JSON.parse(raw);
|
|
504
|
-
if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
|
|
505
|
-
return null;
|
|
506
|
-
return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
|
|
507
|
-
} catch {
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
static #isAlive(pid) {
|
|
512
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
513
|
-
return false;
|
|
514
|
-
try {
|
|
515
|
-
process.kill(pid, 0);
|
|
516
|
-
return true;
|
|
517
|
-
} catch (error) {
|
|
518
|
-
return error.code === "EPERM";
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// pkgs/@akanjs/devkit/spinner.ts
|
|
524
|
-
import ora from "ora";
|
|
525
|
-
|
|
526
|
-
class Spinner {
|
|
527
|
-
static padding = 12;
|
|
528
|
-
static oraOptions = { discardStdin: false };
|
|
529
|
-
static canAnimate(stream = process.stderr) {
|
|
530
|
-
return !stream.isTTY || stream.columns > 0;
|
|
531
|
-
}
|
|
532
|
-
spinner;
|
|
533
|
-
stopWatch = null;
|
|
534
|
-
startAt = new Date;
|
|
535
|
-
prefix;
|
|
536
|
-
message;
|
|
537
|
-
enableSpin;
|
|
538
|
-
constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
|
|
539
|
-
Spinner.padding = Math.max(Spinner.padding, prefix.length);
|
|
540
|
-
this.prefix = prefix;
|
|
541
|
-
this.message = message;
|
|
542
|
-
this.spinner = ora({ ...Spinner.oraOptions, text: message });
|
|
543
|
-
this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
|
|
544
|
-
this.spinner.indent = indent;
|
|
545
|
-
this.enableSpin = enableSpin && Spinner.canAnimate();
|
|
546
|
-
}
|
|
547
|
-
start() {
|
|
548
|
-
this.startAt = new Date;
|
|
549
|
-
if (this.enableSpin) {
|
|
550
|
-
this.spinner.start();
|
|
551
|
-
this.stopWatch = setInterval(() => {
|
|
552
|
-
this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
|
|
553
|
-
this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
|
|
554
|
-
}, 1000);
|
|
555
|
-
} else
|
|
556
|
-
this.spinner.info();
|
|
557
|
-
return this;
|
|
558
|
-
}
|
|
559
|
-
succeed(message) {
|
|
560
|
-
this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
|
|
561
|
-
this.#reset();
|
|
562
|
-
}
|
|
563
|
-
fail(message) {
|
|
564
|
-
this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
|
|
565
|
-
this.#reset();
|
|
566
|
-
}
|
|
567
|
-
isSpinning() {
|
|
568
|
-
return this.spinner.isSpinning;
|
|
569
|
-
}
|
|
570
|
-
#reset() {
|
|
571
|
-
if (this.stopWatch)
|
|
572
|
-
clearInterval(this.stopWatch);
|
|
573
|
-
this.stopWatch = null;
|
|
574
|
-
}
|
|
575
|
-
#getElapsedTimeStr({ floor = false } = {}) {
|
|
576
|
-
const ms = Date.now() - this.startAt.getTime();
|
|
577
|
-
if (ms < 1000)
|
|
578
|
-
return `${ms}ms`;
|
|
579
|
-
const s = Math.floor(ms / 1000);
|
|
580
|
-
if (s < 60)
|
|
581
|
-
return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
|
|
582
|
-
const m = Math.floor(s / 60);
|
|
583
|
-
return `${m}m ${s % 60}s`;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// pkgs/@akanjs/devkit/executors.ts
|
|
588
|
-
import {
|
|
589
|
-
exec,
|
|
590
|
-
fork,
|
|
591
|
-
spawn as spawn2
|
|
592
|
-
} from "child_process";
|
|
593
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
594
|
-
import {
|
|
595
|
-
copyFile,
|
|
596
|
-
cp as cpEntry,
|
|
597
|
-
lstat,
|
|
598
|
-
mkdir as mkdir2,
|
|
599
|
-
readdir as readDirEntries,
|
|
600
|
-
realpath,
|
|
601
|
-
rm as rm3,
|
|
602
|
-
stat as stat2,
|
|
603
|
-
symlink
|
|
604
|
-
} from "fs/promises";
|
|
605
|
-
import path12 from "path";
|
|
606
|
-
import { pathToFileURL } from "url";
|
|
607
|
-
import {
|
|
608
|
-
capitalize,
|
|
609
|
-
getPageSourceFileViolation,
|
|
610
|
-
isRouteSourceFile,
|
|
611
|
-
Logger as Logger2,
|
|
612
|
-
parseRouteModuleKey,
|
|
613
|
-
validatePageSourceFile,
|
|
614
|
-
validateSubRoutePageKey
|
|
615
|
-
} from "akanjs/common";
|
|
616
|
-
var {$ } = globalThis.Bun;
|
|
617
|
-
import chalk2 from "chalk";
|
|
618
|
-
|
|
619
|
-
// pkgs/@akanjs/devkit/agentsIndex.ts
|
|
620
|
-
var recipeScannerLoad = null;
|
|
621
|
-
var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
|
|
622
|
-
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
623
|
-
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
624
|
-
var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
625
|
-
var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
|
|
626
|
-
|
|
627
|
-
${block}`;
|
|
628
|
-
var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
629
|
-
var readDevkitVersion = async () => {
|
|
630
|
-
const { readFile } = await import("fs/promises");
|
|
631
|
-
const { getDirname } = await import("./fileSys-wzzrntkw.js");
|
|
632
|
-
try {
|
|
633
|
-
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
634
|
-
return JSON.parse(raw).version ?? null;
|
|
635
|
-
} catch {
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
var upsertAgentBlock = (existing, block) => {
|
|
640
|
-
const managed = `${AGENT_BLOCK_START}
|
|
641
|
-
${block}
|
|
642
|
-
${AGENT_BLOCK_END}`;
|
|
643
|
-
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
644
|
-
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
645
|
-
if (startIndex >= 0 && endIndex > startIndex) {
|
|
646
|
-
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
647
|
-
}
|
|
648
|
-
return `${existing.replace(/\s*$/, "")}
|
|
649
|
-
|
|
650
|
-
${managed}
|
|
651
|
-
`;
|
|
652
|
-
};
|
|
653
|
-
var extractAgentBlock = (content) => {
|
|
654
|
-
const startIndex = content.indexOf(AGENT_BLOCK_START);
|
|
655
|
-
const endIndex = content.indexOf(AGENT_BLOCK_END);
|
|
656
|
-
if (startIndex < 0 || endIndex <= startIndex)
|
|
657
|
-
return null;
|
|
658
|
-
return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
|
|
659
|
-
};
|
|
660
|
-
var signatureOf = (recipe) => {
|
|
661
|
-
const entries = Object.entries(recipe.variants);
|
|
662
|
-
if (entries.length === 0)
|
|
663
|
-
return "";
|
|
664
|
-
const parts = entries.map(([key, values]) => {
|
|
665
|
-
if (values.length === 1 && values[0] === "true")
|
|
666
|
-
return `${key}?`;
|
|
667
|
-
const def = recipe.defaultVariants?.[key];
|
|
668
|
-
return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
|
|
669
|
-
});
|
|
670
|
-
return `(${parts.join(" \xB7 ")})`;
|
|
671
|
-
};
|
|
672
|
-
var renderRecipeEntries = (recipes) => {
|
|
673
|
-
const groups = new Map;
|
|
674
|
-
for (const recipe of recipes)
|
|
675
|
-
groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
|
|
676
|
-
return [...groups.entries()].map(([importFrom, list]) => {
|
|
677
|
-
const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
|
|
678
|
-
`);
|
|
679
|
-
return `Import from \`${importFrom}\`:
|
|
680
|
-
${items}`;
|
|
681
|
-
}).join(`
|
|
682
|
-
|
|
683
|
-
`);
|
|
684
|
-
};
|
|
685
|
-
var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
|
|
686
|
-
const { collectRecipeSources } = await loadRecipeScanner();
|
|
687
|
-
const sources = [
|
|
688
|
-
...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
|
|
689
|
-
];
|
|
690
|
-
for (const lib of [...new Set(libDeps)].sort()) {
|
|
691
|
-
if (scope.type === "lib" && lib === scope.name)
|
|
692
|
-
continue;
|
|
693
|
-
sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
|
|
694
|
-
}
|
|
695
|
-
return sources;
|
|
696
|
-
};
|
|
697
|
-
var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
|
|
698
|
-
const { scanRecipes } = await loadRecipeScanner();
|
|
699
|
-
return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
|
|
700
|
-
};
|
|
701
|
-
var renderScopeAgentBlock = (scope, recipes) => {
|
|
702
|
-
const scopePath = `${scope.type}s/${scope.name}`;
|
|
703
|
-
const intro = `## Recipes In Scope
|
|
704
|
-
|
|
705
|
-
UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
|
|
706
|
-
\`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
|
|
707
|
-
arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
|
|
708
|
-
boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
|
|
709
|
-
and verified by \`akan lint ${scope.name}\`.`;
|
|
710
|
-
if (recipes.length === 0) {
|
|
711
|
-
return `${intro}
|
|
712
|
-
|
|
713
|
-
No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
|
|
714
|
-
root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
|
|
715
|
-
folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
|
|
716
|
-
}
|
|
717
|
-
return `${intro}
|
|
718
|
-
|
|
719
|
-
${renderRecipeEntries(recipes)}`;
|
|
720
|
-
};
|
|
721
|
-
var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
|
|
722
|
-
|
|
723
|
-
Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
|
|
724
|
-
root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
|
|
725
|
-
section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
|
|
726
|
-
markers freely.
|
|
727
|
-
|
|
728
|
-
${AGENT_BLOCK_START}
|
|
729
|
-
${block}
|
|
730
|
-
${AGENT_BLOCK_END}
|
|
731
|
-
`;
|
|
732
|
-
var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
|
|
733
|
-
|
|
734
|
-
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
735
|
-
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
736
|
-
|
|
737
|
-
Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
|
|
738
|
-
|
|
739
|
-
- Restates the identifier, the signature, or the line under it \u2192 delete it.
|
|
740
|
-
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
|
|
741
|
-
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
|
|
742
|
-
- Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
|
|
743
|
-
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
744
|
-
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
|
|
745
|
-
keep it, one line.
|
|
746
|
-
|
|
747
|
-
That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
748
|
-
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
749
|
-
surrounding code carries none, your diff carries none.`;
|
|
750
|
-
var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
|
|
751
|
-
|
|
752
|
-
@AGENTS.md
|
|
753
|
-
|
|
754
|
-
${CLAUDE_COMMENT_RULE}
|
|
755
|
-
`;
|
|
756
|
-
|
|
757
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
758
|
-
import path5 from "path";
|
|
759
|
-
|
|
760
|
-
// pkgs/@akanjs/devkit/akanConfig/types.ts
|
|
761
|
-
import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
|
|
762
|
-
|
|
763
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
764
|
-
class AkanBin {
|
|
765
|
-
static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
|
|
766
|
-
static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
767
|
-
static parse(declared, owner, baseDir) {
|
|
768
|
-
if (declared === undefined)
|
|
769
|
-
return {};
|
|
770
|
-
if (!declared || typeof declared !== "object" || Array.isArray(declared))
|
|
771
|
-
throw new Error(`${owner}: bin maps each executable's name to its platforms`);
|
|
772
|
-
return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
|
|
773
|
-
}
|
|
774
|
-
static isArchive(source) {
|
|
775
|
-
return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
|
|
776
|
-
}
|
|
777
|
-
static #platforms(name, platforms, owner, baseDir) {
|
|
778
|
-
if (!AkanBin.fileName.test(name))
|
|
779
|
-
throw new Error(`${owner}: bin.${name} is not a file name`);
|
|
780
|
-
if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
|
|
781
|
-
throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
|
|
782
|
-
const resolved = {};
|
|
783
|
-
for (const [platform, source] of Object.entries(platforms)) {
|
|
784
|
-
if (!binPlatforms.includes(platform))
|
|
785
|
-
throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
|
|
786
|
-
resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
|
|
787
|
-
}
|
|
788
|
-
return resolved;
|
|
789
|
-
}
|
|
790
|
-
static #source(at, source, baseDir) {
|
|
791
|
-
if (!source || typeof source !== "object" || Array.isArray(source))
|
|
792
|
-
throw new Error(`${at} must be { url, sha256 } or { path }`);
|
|
793
|
-
const { url, sha256, path: localPath, file, ...rest } = source;
|
|
794
|
-
const extra = Object.keys(rest);
|
|
795
|
-
if (extra.length)
|
|
796
|
-
throw new Error(`${at}: unknown key ${extra.join(", ")}`);
|
|
797
|
-
if (url === undefined === (localPath === undefined))
|
|
798
|
-
throw new Error(`${at} takes either url and sha256, or path`);
|
|
799
|
-
const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
|
|
800
|
-
const archive = AkanBin.isArchive(resolved);
|
|
801
|
-
if (file === undefined) {
|
|
802
|
-
if (archive)
|
|
803
|
-
throw new Error(`${at} names an archive: file picks the executable inside it`);
|
|
804
|
-
return resolved;
|
|
805
|
-
}
|
|
806
|
-
if (!archive)
|
|
807
|
-
throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
|
|
808
|
-
const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
|
|
809
|
-
if (!parts.length || path5.isAbsolute(file) || parts.includes(".."))
|
|
810
|
-
throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
|
|
811
|
-
return { ...resolved, file: parts.join("/") };
|
|
812
|
-
}
|
|
813
|
-
static #urlSource(at, url, sha256) {
|
|
814
|
-
if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
|
|
815
|
-
throw new Error(`${at}: url must be an http(s) address`);
|
|
816
|
-
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
|
|
817
|
-
throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
|
|
818
|
-
return { url, sha256: sha256.toLowerCase() };
|
|
819
|
-
}
|
|
820
|
-
static #pathSource(at, localPath, sha256, baseDir) {
|
|
821
|
-
if (typeof localPath !== "string" || !localPath)
|
|
822
|
-
throw new Error(`${at}: path must name a file`);
|
|
823
|
-
if (sha256 !== undefined)
|
|
824
|
-
throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
|
|
825
|
-
return { path: path5.resolve(baseDir, localPath) };
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
|
|
829
|
-
import fs2 from "fs";
|
|
830
|
-
import path6 from "path";
|
|
831
|
-
import { fileURLToPath } from "url";
|
|
832
|
-
import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
|
|
833
|
-
import { resolveAkanI18nConfig } from "akanjs/common";
|
|
834
|
-
var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
835
|
-
var DEFAULT_OPTIMIZE_IMPORTS = [
|
|
836
|
-
"lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
|
|
837
|
-
"@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
|
|
838
|
-
"@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
|
|
839
|
-
"@material-ui/icons @tabler/icons-react mui-core react-icons/*"
|
|
840
|
-
].flatMap((line) => line.split(" "));
|
|
841
|
-
var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
|
|
842
|
-
var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
843
|
-
var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
|
|
844
|
-
var DEFAULT_AKAN_IMAGE_CONFIG = {
|
|
845
|
-
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
846
|
-
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
847
|
-
formats: ["image/webp"],
|
|
848
|
-
qualities: [75],
|
|
849
|
-
minimumCacheTTL: 14400,
|
|
850
|
-
remotePatterns: [],
|
|
851
|
-
localPatterns: [{ pathname: "/**" }],
|
|
852
|
-
dangerouslyAllowSVG: false,
|
|
853
|
-
maximumRedirects: 3,
|
|
854
|
-
fetchTimeoutMs: 7000,
|
|
855
|
-
maxRemoteBytes: 25 * 1024 * 1024,
|
|
856
|
-
maxConcurrency: 0
|
|
857
|
-
};
|
|
858
|
-
var normalizeIndexPath = (indexPath) => {
|
|
859
|
-
const normalized = indexPath?.trim();
|
|
860
|
-
if (!normalized)
|
|
861
|
-
return;
|
|
862
|
-
const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
863
|
-
return path === "/" ? "/" : path;
|
|
864
|
-
};
|
|
865
|
-
var normalizeStringList = (values) => {
|
|
866
|
-
const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
867
|
-
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
|
868
|
-
};
|
|
869
|
-
var sanitizeAppIdSegment = (value) => {
|
|
870
|
-
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
871
|
-
if (!cleaned)
|
|
872
|
-
return "app";
|
|
873
|
-
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
|
|
874
|
-
};
|
|
875
|
-
var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
|
|
876
|
-
var normalizeDeepLinkDomain = (domain) => {
|
|
877
|
-
const normalized = domain.trim();
|
|
878
|
-
if (!normalized)
|
|
879
|
-
return "";
|
|
880
|
-
try {
|
|
881
|
-
const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
|
|
882
|
-
return url.host.toLowerCase();
|
|
883
|
-
} catch {
|
|
884
|
-
return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
var normalizeDeepLinks = (deepLinks) => {
|
|
888
|
-
const schemes = normalizeStringList(deepLinks?.schemes);
|
|
889
|
-
const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
|
|
890
|
-
if (!schemes && !domains)
|
|
891
|
-
return;
|
|
892
|
-
return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
|
|
893
|
-
};
|
|
894
|
-
var emptyLibContributions = () => ({
|
|
895
|
-
externalLibs: [],
|
|
896
|
-
trustedDependencies: [],
|
|
897
|
-
bin: [],
|
|
898
|
-
docker: { preRuns: [], postRuns: [] },
|
|
899
|
-
keepFonts: []
|
|
900
|
-
});
|
|
901
|
-
var normalizePackageNames = (names, owner) => {
|
|
902
|
-
if (names === undefined)
|
|
903
|
-
return [];
|
|
904
|
-
if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
|
|
905
|
-
throw new Error(`${owner}: trustedDependencies lists package names`);
|
|
906
|
-
return [...new Set(names.map((name) => name.trim()))];
|
|
907
|
-
};
|
|
908
|
-
var normalizeKeepFonts = (keepFonts) => [
|
|
909
|
-
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
|
|
910
|
-
];
|
|
911
|
-
var dedupeDockerRuns = (runs) => {
|
|
912
|
-
const byKey = new Map;
|
|
913
|
-
for (const run of runs)
|
|
914
|
-
byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
915
|
-
return [...byKey.values()];
|
|
916
|
-
};
|
|
917
600
|
|
|
918
601
|
class AkanAppConfig {
|
|
919
602
|
app;
|
|
@@ -1025,6 +708,8 @@ class AkanAppConfig {
|
|
|
1025
708
|
const { updates } = merged;
|
|
1026
709
|
if (updates && (!updates.url || !(updates.publicKey || updates.publicKeys?.length)))
|
|
1027
710
|
throw new Error(`${where}.updates in ${configPath} has no url or publicKeys${named ? "; give them in native.updates or the target's own" : ""}.`);
|
|
711
|
+
if (updates?.publicKey !== undefined && updates.publicKeys !== undefined)
|
|
712
|
+
throw new Error(`${where}.updates in ${configPath} has both publicKey and publicKeys${named ? ", native.updates merged in: give one, in native.updates' own form when overriding its keys" : ": give one"}.`);
|
|
1028
713
|
AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
|
|
1029
714
|
const {
|
|
1030
715
|
basePath: _basePath,
|
|
@@ -1100,6 +785,8 @@ class AkanAppConfig {
|
|
|
1100
785
|
"AKAN_DATABASE_MODES",
|
|
1101
786
|
"AKAN_SSR",
|
|
1102
787
|
"AKAN_CSR",
|
|
788
|
+
"AKAN_MCP",
|
|
789
|
+
"AKAN_SHUTDOWN_TIMEOUT_MS",
|
|
1103
790
|
"AKAN_MODULES"
|
|
1104
791
|
]);
|
|
1105
792
|
static #desktopServerEnv(env, where, configPath) {
|
|
@@ -1118,11 +805,14 @@ class AkanAppConfig {
|
|
|
1118
805
|
static #desktopServerWatchdog(watchdog, where, configPath) {
|
|
1119
806
|
if (watchdog === undefined || watchdog === false)
|
|
1120
807
|
return null;
|
|
1121
|
-
const
|
|
1122
|
-
const whole = (value) => Number.isInteger(value) && value > 0;
|
|
1123
|
-
if (
|
|
808
|
+
const options = watchdog === true ? {} : watchdog;
|
|
809
|
+
const whole = (value) => value === undefined || Number.isInteger(value) && value > 0;
|
|
810
|
+
if (!AkanAppConfig.#isPlainObject(options) || Object.keys(options).some((key) => key !== "intervalMs" && key !== "failures") || !whole(options.intervalMs) || !whole(options.failures))
|
|
1124
811
|
throw new Error(`${where}.watchdog in ${configPath} must be true or { intervalMs, failures }, whole numbers from 1.`);
|
|
1125
|
-
return {
|
|
812
|
+
return {
|
|
813
|
+
intervalMs: options.intervalMs ?? 1e4,
|
|
814
|
+
failures: options.failures ?? 3
|
|
815
|
+
};
|
|
1126
816
|
}
|
|
1127
817
|
static #platformIndexPath(section) {
|
|
1128
818
|
const { indexPath: rawIndexPath, ...rest } = section;
|
|
@@ -1242,7 +932,7 @@ class AkanAppConfig {
|
|
|
1242
932
|
}
|
|
1243
933
|
#applyRoutes(routes = []) {
|
|
1244
934
|
for (const route of routes) {
|
|
1245
|
-
const basePath = route.basePath ? route.basePath
|
|
935
|
+
const basePath = route.basePath ? trimSlashes(route.basePath) : null;
|
|
1246
936
|
if (basePath !== null)
|
|
1247
937
|
this.basePaths.add(basePath);
|
|
1248
938
|
const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
|
|
@@ -1341,232 +1031,670 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
1341
1031
|
const importPath = bustImportCache ? `${configPath}?akanConfigGeneration=${++AkanAppConfig.#importGeneration}` : configPath;
|
|
1342
1032
|
return await import(importPath).then((mod) => mod.default);
|
|
1343
1033
|
}
|
|
1344
|
-
static async from(app, { bustImportCache = false } = {}) {
|
|
1345
|
-
const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
|
|
1346
|
-
AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
|
|
1347
|
-
WorkspaceExecutor.getBaseDevEnv(
|
|
1348
|
-
app.workspace.getLibs(),
|
|
1349
|
-
app.workspace.getPackageJson()
|
|
1350
|
-
]);
|
|
1351
|
-
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
1352
|
-
const {
|
|
1353
|
-
plugins,
|
|
1354
|
-
pipeline: _pipeline,
|
|
1355
|
-
...config
|
|
1356
|
-
} = resolved ?? {};
|
|
1034
|
+
static async from(app, { bustImportCache = false } = {}) {
|
|
1035
|
+
const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
|
|
1036
|
+
AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
|
|
1037
|
+
WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
|
|
1038
|
+
app.workspace.getLibs(),
|
|
1039
|
+
app.workspace.getPackageJson()
|
|
1040
|
+
]);
|
|
1041
|
+
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
1042
|
+
const {
|
|
1043
|
+
plugins,
|
|
1044
|
+
pipeline: _pipeline,
|
|
1045
|
+
...config
|
|
1046
|
+
} = resolved ?? {};
|
|
1047
|
+
if ("subspace" in config)
|
|
1048
|
+
throw new Error(`apps/${app.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
|
|
1049
|
+
const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
|
|
1050
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
1051
|
+
}
|
|
1052
|
+
static async#collectLibContributions(app, libs, bustImportCache) {
|
|
1053
|
+
const libConfigs = await Promise.all(libs.map(async (libName) => LibExecutor.from(app, libName).getConfig({ refresh: bustImportCache }).catch((error) => {
|
|
1054
|
+
if (error instanceof LibConfigRefusal)
|
|
1055
|
+
throw error;
|
|
1056
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
|
|
1057
|
+
return null;
|
|
1058
|
+
})));
|
|
1059
|
+
return {
|
|
1060
|
+
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
1061
|
+
trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
|
|
1062
|
+
bin: libConfigs.flatMap((libConfig) => libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : []),
|
|
1063
|
+
docker: {
|
|
1064
|
+
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
1065
|
+
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? [])
|
|
1066
|
+
},
|
|
1067
|
+
keepFonts: libConfigs.flatMap((libConfig) => (libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`))
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
#resolveProductionDependencyVersion(lib) {
|
|
1071
|
+
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
1072
|
+
if (rootVersion)
|
|
1073
|
+
return rootVersion;
|
|
1074
|
+
const akanPackageJson = getAkanPackageJson();
|
|
1075
|
+
const version = akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
1076
|
+
if (!version)
|
|
1077
|
+
throw new Error(`Dependency ${lib} not found in package.json`);
|
|
1078
|
+
return version;
|
|
1079
|
+
}
|
|
1080
|
+
#getProductionRuntimePackages() {
|
|
1081
|
+
return [
|
|
1082
|
+
...this.externalLibs,
|
|
1083
|
+
...SSR_RUNTIME_PACKAGES,
|
|
1084
|
+
...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode))
|
|
1085
|
+
];
|
|
1086
|
+
}
|
|
1087
|
+
getDatabaseModeRuntimePackages(databaseMode) {
|
|
1088
|
+
return [...DatabaseModes.drivers[databaseMode]];
|
|
1089
|
+
}
|
|
1090
|
+
getMissingDatabaseModeDependencySpecs(databaseMode) {
|
|
1091
|
+
return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
|
|
1092
|
+
}
|
|
1093
|
+
resolveDatabaseMode() {
|
|
1094
|
+
return DatabaseModes.resolve({
|
|
1095
|
+
requested: process.env.AKAN_DATABASE_MODE,
|
|
1096
|
+
declared: this.database.modes.join(","),
|
|
1097
|
+
local: true
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
static #database(app, config) {
|
|
1101
|
+
const declared = config?.database?.modes?.filter((mode) => !!mode);
|
|
1102
|
+
const modes = declared?.length ? declared : ["single"];
|
|
1103
|
+
return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
|
|
1104
|
+
}
|
|
1105
|
+
#getMissingDependencySpecs(libs) {
|
|
1106
|
+
const rootDependencies = {
|
|
1107
|
+
...this.rootPackageJson.dependencies,
|
|
1108
|
+
...this.rootPackageJson.devDependencies
|
|
1109
|
+
};
|
|
1110
|
+
return libs.filter((lib) => !rootDependencies[lib]).map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
|
|
1111
|
+
}
|
|
1112
|
+
get akanVersion() {
|
|
1113
|
+
return AkanAppConfig.akanVersion;
|
|
1114
|
+
}
|
|
1115
|
+
static get akanVersion() {
|
|
1116
|
+
return getAkanPackageJson().version;
|
|
1117
|
+
}
|
|
1118
|
+
getProductionPackageJson(data = {}) {
|
|
1119
|
+
return {
|
|
1120
|
+
name: this.app.name,
|
|
1121
|
+
description: this.app.name,
|
|
1122
|
+
version: "1.0.0",
|
|
1123
|
+
main: "./main.js",
|
|
1124
|
+
dependencies: Object.fromEntries([...new Set(this.#getProductionRuntimePackages())].map((lib) => [
|
|
1125
|
+
lib,
|
|
1126
|
+
this.#resolveProductionDependencyVersion(lib)
|
|
1127
|
+
])),
|
|
1128
|
+
...this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {},
|
|
1129
|
+
...data
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
var akanPackageJson = null;
|
|
1134
|
+
function getAkanPackageJson() {
|
|
1135
|
+
if (akanPackageJson)
|
|
1136
|
+
return akanPackageJson;
|
|
1137
|
+
const sourceDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
1138
|
+
const packageJsonPaths = [
|
|
1139
|
+
path3.join(sourceDir, "../../../akanjs/package.json"),
|
|
1140
|
+
path3.join(process.cwd(), "pkgs/akanjs/package.json"),
|
|
1141
|
+
path3.join(path3.dirname(Bun.main), "node_modules/akanjs/package.json")
|
|
1142
|
+
];
|
|
1143
|
+
try {
|
|
1144
|
+
packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
|
|
1145
|
+
} catch {}
|
|
1146
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
1147
|
+
try {
|
|
1148
|
+
akanPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
1149
|
+
return akanPackageJson;
|
|
1150
|
+
} catch {}
|
|
1151
|
+
}
|
|
1152
|
+
akanPackageJson = {
|
|
1153
|
+
name: "akanjs",
|
|
1154
|
+
version: "0.0.0",
|
|
1155
|
+
description: "akanjs",
|
|
1156
|
+
dependencies: {}
|
|
1157
|
+
};
|
|
1158
|
+
return akanPackageJson;
|
|
1159
|
+
}
|
|
1160
|
+
function mergeImageConfig(config = {}) {
|
|
1161
|
+
return {
|
|
1162
|
+
...DEFAULT_AKAN_IMAGE_CONFIG,
|
|
1163
|
+
...config,
|
|
1164
|
+
deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
|
|
1165
|
+
imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
|
|
1166
|
+
formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
|
|
1167
|
+
qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
|
|
1168
|
+
remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
|
|
1169
|
+
localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
class AkanLibConfig {
|
|
1174
|
+
lib;
|
|
1175
|
+
externalLibs;
|
|
1176
|
+
trustedDependencies;
|
|
1177
|
+
bin;
|
|
1178
|
+
docker;
|
|
1179
|
+
assets;
|
|
1180
|
+
plugins;
|
|
1181
|
+
constructor(lib, config, plugins = []) {
|
|
1182
|
+
this.lib = lib;
|
|
1183
|
+
this.externalLibs = config?.externalLibs ?? [];
|
|
1184
|
+
const owner = `libs/${lib.name}/akan.config.ts`;
|
|
1185
|
+
this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
|
|
1186
|
+
this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
|
|
1187
|
+
this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
|
|
1188
|
+
this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
|
|
1189
|
+
this.plugins = plugins;
|
|
1190
|
+
}
|
|
1191
|
+
static async from(lib, { bustImportCache = false } = {}) {
|
|
1192
|
+
const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
|
|
1193
|
+
const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
1194
|
+
const { plugins, ...config } = resolved ?? {};
|
|
1195
|
+
if ("pipeline" in config)
|
|
1196
|
+
throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`pipeline\`, which a lib does not take: pipelines belong to apps and the workspace root, and a lib's build-time work goes in an akan build plugin.`);
|
|
1357
1197
|
if ("subspace" in config)
|
|
1358
|
-
throw new
|
|
1359
|
-
|
|
1360
|
-
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
1198
|
+
throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
|
|
1199
|
+
return new AkanLibConfig(lib, config, plugins ?? []);
|
|
1361
1200
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1201
|
+
}
|
|
1202
|
+
//! need to refactor
|
|
1203
|
+
var shiftBuildNum = async (app, delta) => {
|
|
1204
|
+
const { buildNum } = (await AkanAppConfig.from(app)).native;
|
|
1205
|
+
const akanConfigPath = path3.join(app.cwdPath, "akan.config.ts");
|
|
1206
|
+
const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
|
|
1207
|
+
fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
|
|
1208
|
+
};
|
|
1209
|
+
var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
|
|
1210
|
+
var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
|
|
1211
|
+
// pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
|
|
1212
|
+
import path4 from "path";
|
|
1213
|
+
class AkanWorkspaceConfig {
|
|
1214
|
+
static fileName = "akan.config.ts";
|
|
1215
|
+
static #keys = ["subspace", "pipeline", "cloud"];
|
|
1216
|
+
static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
|
|
1217
|
+
static async from(workspace, { bustImportCache = false } = {}) {
|
|
1218
|
+
const file = path4.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
|
|
1219
|
+
if (!await FileSys2.fileExists(file))
|
|
1220
|
+
return new AkanWorkspaceConfig(null, {});
|
|
1221
|
+
const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
|
|
1222
|
+
return new AkanWorkspaceConfig(file, input);
|
|
1223
|
+
}
|
|
1224
|
+
file;
|
|
1225
|
+
subspace;
|
|
1226
|
+
pipeline;
|
|
1227
|
+
cloudHost;
|
|
1228
|
+
constructor(file, input) {
|
|
1229
|
+
this.file = file;
|
|
1230
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
1231
|
+
throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
|
|
1232
|
+
for (const key of Object.keys(input))
|
|
1233
|
+
if (!AkanWorkspaceConfig.#keys.includes(key))
|
|
1234
|
+
throw new Error(`The workspace root akan.config.ts declares \`${key}\`, which it does not take: it holds \`subspace\`, \`pipeline\` and \`cloud\`, and app or lib settings belong in apps/<app>/akan.config.ts or libs/<lib>/akan.config.ts.`);
|
|
1235
|
+
const config = input;
|
|
1236
|
+
this.subspace = config.subspace ?? null;
|
|
1237
|
+
this.pipeline = config.pipeline ?? null;
|
|
1238
|
+
this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
|
|
1239
|
+
}
|
|
1240
|
+
static #cloudHost(cloud) {
|
|
1241
|
+
if (cloud === undefined)
|
|
1365
1242
|
return null;
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
1369
|
-
trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
|
|
1370
|
-
bin: libConfigs.flatMap((libConfig) => libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : []),
|
|
1371
|
-
docker: {
|
|
1372
|
-
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
1373
|
-
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? [])
|
|
1374
|
-
},
|
|
1375
|
-
keepFonts: libConfigs.flatMap((libConfig) => (libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`))
|
|
1243
|
+
const refuse = (why) => {
|
|
1244
|
+
throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
|
|
1376
1245
|
};
|
|
1246
|
+
if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
|
|
1247
|
+
return refuse("that is not an object");
|
|
1248
|
+
const unknown = Object.keys(cloud).filter((key) => key !== "host");
|
|
1249
|
+
if (unknown.length)
|
|
1250
|
+
return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
|
|
1251
|
+
const { host } = cloud;
|
|
1252
|
+
if (host === undefined)
|
|
1253
|
+
return null;
|
|
1254
|
+
const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
|
|
1255
|
+
if (!url || url.search || url.hash || url.username || url.password)
|
|
1256
|
+
return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
|
|
1257
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
|
|
1258
|
+
return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
|
|
1259
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
1377
1260
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1261
|
+
}
|
|
1262
|
+
// pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
|
|
1263
|
+
import { readdir, rm } from "fs/promises";
|
|
1264
|
+
import path6 from "path";
|
|
1265
|
+
|
|
1266
|
+
// pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
|
|
1267
|
+
import fs2 from "fs";
|
|
1268
|
+
import path5 from "path";
|
|
1269
|
+
|
|
1270
|
+
class AsyncDefaultExportDetector {
|
|
1271
|
+
static #typescriptLoad;
|
|
1272
|
+
static #loadTypescript() {
|
|
1273
|
+
AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
|
|
1274
|
+
return AsyncDefaultExportDetector.#typescriptLoad;
|
|
1387
1275
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1276
|
+
static async detect(moduleAbsPath) {
|
|
1277
|
+
try {
|
|
1278
|
+
const typescript = await AsyncDefaultExportDetector.#loadTypescript();
|
|
1279
|
+
const source = fs2.readFileSync(path5.resolve(moduleAbsPath), "utf8");
|
|
1280
|
+
const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
|
|
1281
|
+
return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
|
|
1282
|
+
} catch {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1394
1285
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1286
|
+
#ts;
|
|
1287
|
+
constructor(typescript) {
|
|
1288
|
+
this.#ts = typescript;
|
|
1397
1289
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1290
|
+
detectInSourceFile(sourceFile) {
|
|
1291
|
+
const ts = this.#ts;
|
|
1292
|
+
const asyncBindings = new Map;
|
|
1293
|
+
let defaultIdentifier = null;
|
|
1294
|
+
for (const statement of sourceFile.statements) {
|
|
1295
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
1296
|
+
if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
|
|
1297
|
+
return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
|
|
1298
|
+
}
|
|
1299
|
+
if (statement.name) {
|
|
1300
|
+
asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
|
|
1301
|
+
}
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (ts.isVariableStatement(statement)) {
|
|
1305
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1306
|
+
if (!ts.isIdentifier(declaration.name))
|
|
1307
|
+
continue;
|
|
1308
|
+
asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
|
|
1309
|
+
}
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (ts.isExportAssignment(statement)) {
|
|
1313
|
+
if (this.#isAsyncFunctionExpression(statement.expression))
|
|
1314
|
+
return true;
|
|
1315
|
+
if (ts.isCallExpression(statement.expression))
|
|
1316
|
+
return true;
|
|
1317
|
+
if (ts.isIdentifier(statement.expression))
|
|
1318
|
+
defaultIdentifier = statement.expression.text;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
1322
|
+
for (const specifier of statement.exportClause.elements) {
|
|
1323
|
+
if (specifier.name.text !== "default")
|
|
1324
|
+
continue;
|
|
1325
|
+
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
1400
1330
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1331
|
+
#hasModifier(node, kind) {
|
|
1332
|
+
const ts = this.#ts;
|
|
1333
|
+
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
1334
|
+
}
|
|
1335
|
+
#isAsyncFunctionExpression(node) {
|
|
1336
|
+
const ts = this.#ts;
|
|
1337
|
+
return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
|
|
1342
|
+
var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
|
|
1343
|
+
var IMPLICIT_LAYOUT_DIR = path6.join(".akan", "generated", "root-layouts");
|
|
1344
|
+
var IMPLICIT_DICT_DIR = path6.join(".akan", "generated", "dict");
|
|
1345
|
+
var IMPLICIT_OVERRIDES_DIR = path6.join(".akan", "generated", "overrides");
|
|
1346
|
+
var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
|
|
1347
|
+
var importSpecifier = (fromAbsPath, toAbsPath) => {
|
|
1348
|
+
const rel = path6.relative(path6.dirname(fromAbsPath), toAbsPath).split(path6.sep).join("/");
|
|
1349
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1350
|
+
};
|
|
1351
|
+
async function writeGeneratedOverridesLayoutFile(opts) {
|
|
1352
|
+
const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
|
|
1353
|
+
const absPath = path6.join(path6.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
|
|
1354
|
+
const source = `"use client";
|
|
1355
|
+
import { UiOverrideProvider } from "akanjs/ui";
|
|
1356
|
+
import { createElement, type ReactNode } from "react";
|
|
1357
|
+
import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
|
|
1358
|
+
|
|
1359
|
+
export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
|
|
1360
|
+
return createElement(UiOverrideProvider, { value }, children);
|
|
1361
|
+
}
|
|
1362
|
+
`;
|
|
1363
|
+
await Bun.write(absPath, source);
|
|
1364
|
+
return absPath;
|
|
1365
|
+
}
|
|
1366
|
+
async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
|
|
1367
|
+
const keep = new Set(keepAbsPaths);
|
|
1368
|
+
const names = await readdir(dirAbsPath).catch(() => []);
|
|
1369
|
+
await Promise.all(names.map((name) => path6.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm(absPath, { force: true })));
|
|
1370
|
+
}
|
|
1371
|
+
function getRootBoundarySegments(key) {
|
|
1372
|
+
const match = LAYOUT_KEY_RE.exec(key);
|
|
1373
|
+
if (!match)
|
|
1374
|
+
return null;
|
|
1375
|
+
const prefix = match[1]?.replace(/\/$/, "");
|
|
1376
|
+
if (!prefix)
|
|
1377
|
+
return [];
|
|
1378
|
+
return prefix.split("/").filter(Boolean);
|
|
1379
|
+
}
|
|
1380
|
+
function implicitRootLayoutKey(segments) {
|
|
1381
|
+
return `./${[...segments, "__root_layout"].join("/")}.tsx`;
|
|
1382
|
+
}
|
|
1383
|
+
function implicitRootLayoutAbsPath(appCwdPath, segments) {
|
|
1384
|
+
const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
|
|
1385
|
+
return path6.join(path6.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
|
|
1386
|
+
}
|
|
1387
|
+
function isRootBoundarySegments(segments, basePaths) {
|
|
1388
|
+
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
1389
|
+
if (firstVisibleIndex === -1)
|
|
1390
|
+
return segments.length <= 1;
|
|
1391
|
+
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
|
|
1392
|
+
return false;
|
|
1393
|
+
const visible = segments.slice(firstVisibleIndex);
|
|
1394
|
+
const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
|
|
1395
|
+
return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
|
|
1396
|
+
}
|
|
1397
|
+
function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
|
|
1398
|
+
const boundaries = new Map;
|
|
1399
|
+
for (const key of pageKeys) {
|
|
1400
|
+
const segments = getRootBoundarySegments(key);
|
|
1401
|
+
if (!segments)
|
|
1402
|
+
continue;
|
|
1403
|
+
if (!isRootBoundarySegments(segments, basePaths))
|
|
1404
|
+
continue;
|
|
1405
|
+
const id = segments.join("/");
|
|
1406
|
+
boundaries.set(id, {
|
|
1407
|
+
sourceKey: key,
|
|
1408
|
+
sourceAbsPath: path6.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
|
|
1409
|
+
segments
|
|
1406
1410
|
});
|
|
1407
1411
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
|
|
1412
|
-
}
|
|
1413
|
-
#getMissingDependencySpecs(libs) {
|
|
1414
|
-
const rootDependencies = {
|
|
1415
|
-
...this.rootPackageJson.dependencies,
|
|
1416
|
-
...this.rootPackageJson.devDependencies
|
|
1417
|
-
};
|
|
1418
|
-
return libs.filter((lib) => !rootDependencies[lib]).map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
|
|
1419
|
-
}
|
|
1420
|
-
get akanVersion() {
|
|
1421
|
-
return AkanAppConfig.akanVersion;
|
|
1422
|
-
}
|
|
1423
|
-
static get akanVersion() {
|
|
1424
|
-
return getAkanPackageJson().version;
|
|
1425
|
-
}
|
|
1426
|
-
getProductionPackageJson(data = {}) {
|
|
1427
|
-
return {
|
|
1428
|
-
name: this.app.name,
|
|
1429
|
-
description: this.app.name,
|
|
1430
|
-
version: "1.0.0",
|
|
1431
|
-
main: "./main.js",
|
|
1432
|
-
dependencies: Object.fromEntries([...new Set(this.#getProductionRuntimePackages())].map((lib) => [
|
|
1433
|
-
lib,
|
|
1434
|
-
this.#resolveProductionDependencyVersion(lib)
|
|
1435
|
-
])),
|
|
1436
|
-
...this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {},
|
|
1437
|
-
...data
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1412
|
+
if (boundaries.size === 0)
|
|
1413
|
+
boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
|
|
1414
|
+
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
1440
1415
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
try {
|
|
1456
|
-
akanPackageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
|
|
1457
|
-
return akanPackageJson;
|
|
1458
|
-
} catch {}
|
|
1416
|
+
function hasAncestorRootBoundary(boundary, boundaries) {
|
|
1417
|
+
return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
|
|
1418
|
+
}
|
|
1419
|
+
function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
|
|
1420
|
+
const rootLayoutKey = pageKeys.find((key) => {
|
|
1421
|
+
const segments = getRootBoundarySegments(key);
|
|
1422
|
+
return segments !== null && segments.length === 0;
|
|
1423
|
+
});
|
|
1424
|
+
return rootLayoutKey ? path6.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
|
|
1425
|
+
}
|
|
1426
|
+
async function assertEnvClientConvention(appCwdPath, appName) {
|
|
1427
|
+
const envPath = path6.join(appCwdPath, "env", "env.client.ts");
|
|
1428
|
+
if (!await Bun.file(envPath).exists()) {
|
|
1429
|
+
throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
|
|
1459
1430
|
}
|
|
1460
|
-
akanPackageJson = {
|
|
1461
|
-
name: "akanjs",
|
|
1462
|
-
version: "0.0.0",
|
|
1463
|
-
description: "akanjs",
|
|
1464
|
-
dependencies: {}
|
|
1465
|
-
};
|
|
1466
|
-
return akanPackageJson;
|
|
1467
1431
|
}
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1432
|
+
async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
|
|
1433
|
+
const absPath = path6.join(path6.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
1434
|
+
await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
|
|
1435
|
+
|
|
1436
|
+
export const allDictionary = getAllDictionary();
|
|
1437
|
+
`);
|
|
1438
|
+
return absPath;
|
|
1439
|
+
}
|
|
1440
|
+
async function writeGeneratedRootLayoutFile(opts) {
|
|
1441
|
+
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
1442
|
+
const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
|
|
1443
|
+
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
1444
|
+
const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
|
|
1445
|
+
const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
|
|
1446
|
+
const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
|
|
1447
|
+
const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
|
|
1448
|
+
const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
|
|
1449
|
+
void st;
|
|
1450
|
+
` : `import "@apps/${opts.appName}/client";
|
|
1451
|
+
`;
|
|
1452
|
+
const inheritedLabel = inheritedSourceAbsPath ? path6.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
|
|
1453
|
+
const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
|
|
1454
|
+
const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
|
|
1455
|
+
` : `const inheritedLayout: LayoutModule = {};
|
|
1456
|
+
`;
|
|
1457
|
+
const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
|
|
1458
|
+
const userLabel = opts.boundary.sourceAbsPath ? path6.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
|
|
1459
|
+
const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
|
|
1460
|
+
const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
|
|
1461
|
+
const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
|
|
1462
|
+
` : `const UserLayout = ({ children }: LayoutProps) => children;
|
|
1463
|
+
const userLayout: LayoutModule = {};
|
|
1464
|
+
`;
|
|
1465
|
+
const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
|
|
1466
|
+
const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
|
|
1467
|
+
const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
|
|
1468
|
+
const layoutBinding = isAsyncUserLayout ? ` const layout =
|
|
1469
|
+
process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
|
|
1470
|
+
? await UserLayout({ params, searchParams, children })
|
|
1471
|
+
: ${userLayoutElement};
|
|
1472
|
+
` : "";
|
|
1473
|
+
const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
|
|
1474
|
+
const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
|
|
1475
|
+
const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
|
|
1476
|
+
const layoutExports = `export async function generateHead(props: PageProps) {
|
|
1477
|
+
const heads = await Promise.all(
|
|
1478
|
+
${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
|
|
1479
|
+
);
|
|
1480
|
+
return RouteHead.mergeNodes(heads);
|
|
1479
1481
|
}
|
|
1480
1482
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1483
|
+
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
1484
|
+
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
1485
|
+
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
1486
|
+
export const Loading = userLayout.Loading;
|
|
1487
|
+
|
|
1488
|
+
${layoutSignature}({ children, params, searchParams }: LayoutProps) {
|
|
1489
|
+
${layoutBinding} return `;
|
|
1490
|
+
const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
1491
|
+
import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
|
|
1492
|
+
import type { ReactNode } from "react";
|
|
1493
|
+
import { System } from "akanjs/ui";
|
|
1494
|
+
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
1495
|
+
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
1496
|
+
${clientImport}${inheritedImport}${userImport}
|
|
1497
|
+
const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
|
|
1498
|
+
const defaultFonts = userFonts.filter((font) => font.default);
|
|
1499
|
+
if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
|
|
1500
|
+
const defaultFont = defaultFonts[0];
|
|
1501
|
+
const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
|
|
1502
|
+
export const theme = userLayout.theme ?? inheritedLayout.theme;
|
|
1503
|
+
export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
|
|
1504
|
+
export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
|
|
1505
|
+
|
|
1506
|
+
${layoutExports}(
|
|
1507
|
+
<System.Provider
|
|
1508
|
+
of={GeneratedLayout as never}
|
|
1509
|
+
appName=${JSON.stringify(opts.appName)}
|
|
1510
|
+
${prefix ? `prefix=${JSON.stringify(prefix)}
|
|
1511
|
+
` : ""}params={params}
|
|
1512
|
+
manifest={manifest}
|
|
1513
|
+
env={env}
|
|
1514
|
+
theme={theme}
|
|
1515
|
+
viewport={viewport}
|
|
1516
|
+
fonts={loadFonts(userFonts)}
|
|
1517
|
+
className={defaultFontClassName}
|
|
1518
|
+
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
1519
|
+
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
1520
|
+
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
1521
|
+
allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
|
|
1522
|
+
>
|
|
1523
|
+
${layoutChild}
|
|
1524
|
+
</System.Provider>
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
1528
|
+
import { resolveRouteModule, RouteHead } from "akanjs/client";
|
|
1529
|
+
import type { ReactNode } from "react";
|
|
1530
|
+
${inheritedImport}${userImport}
|
|
1531
|
+
${layoutExports}${layoutReturn};
|
|
1532
|
+
}
|
|
1533
|
+
`;
|
|
1534
|
+
await Bun.write(absPath, source);
|
|
1535
|
+
return absPath;
|
|
1536
|
+
}
|
|
1537
|
+
async function resolveSsrPageEntries(opts) {
|
|
1538
|
+
const absPageDir = path6.resolve(opts.appCwdPath, "page");
|
|
1539
|
+
const hasSt = await Bun.file(path6.join(opts.appCwdPath, "lib", "st.ts")).exists();
|
|
1540
|
+
const basePaths = opts.basePaths ?? [];
|
|
1541
|
+
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
1542
|
+
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
1543
|
+
const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
|
|
1544
|
+
const segments = getRootBoundarySegments(key);
|
|
1545
|
+
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
1546
|
+
}));
|
|
1547
|
+
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
1548
|
+
const userAbsPath = path6.resolve(absPageDir, key);
|
|
1549
|
+
if (OVERRIDES_KEY_RE.test(key)) {
|
|
1550
|
+
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
1551
|
+
appCwdPath: opts.appCwdPath,
|
|
1552
|
+
key,
|
|
1553
|
+
userAbsPath
|
|
1554
|
+
});
|
|
1555
|
+
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
1556
|
+
}
|
|
1557
|
+
return { key, moduleAbsPath: userAbsPath };
|
|
1558
|
+
}));
|
|
1559
|
+
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
1560
|
+
key: implicitRootLayoutKey(boundary.segments),
|
|
1561
|
+
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
1562
|
+
appCwdPath: opts.appCwdPath,
|
|
1563
|
+
appName: opts.appName,
|
|
1564
|
+
boundary,
|
|
1565
|
+
rootSourceAbsPath,
|
|
1566
|
+
includeStInit: hasSt && boundary.segments.length === 0,
|
|
1567
|
+
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
|
|
1568
|
+
}),
|
|
1569
|
+
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
|
|
1570
|
+
})));
|
|
1571
|
+
const appCwdAbsPath = path6.resolve(opts.appCwdPath);
|
|
1572
|
+
await Promise.all([
|
|
1573
|
+
pruneGeneratedFiles(path6.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
|
|
1574
|
+
pruneGeneratedFiles(path6.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
|
|
1575
|
+
]);
|
|
1576
|
+
return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
|
|
1577
|
+
}
|
|
1578
|
+
async function resolveSsrPageEntriesForApp(app, pageKeys) {
|
|
1579
|
+
const config = await app.getConfig();
|
|
1580
|
+
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// pkgs/@akanjs/devkit/codegenLock.ts
|
|
1584
|
+
import { mkdir, open, readFile, rm as rm2, stat } from "fs/promises";
|
|
1585
|
+
import path7 from "path";
|
|
1586
|
+
import { Logger as Logger2 } from "akanjs/common";
|
|
1587
|
+
|
|
1588
|
+
class CodegenLock {
|
|
1589
|
+
static fileName = "codegen.lock";
|
|
1590
|
+
static waitTimeoutMs = 1e4;
|
|
1591
|
+
static unknownHolderStaleMs = 60000;
|
|
1592
|
+
static #pollMs = 25;
|
|
1593
|
+
static #logger = new Logger2("CodegenLock");
|
|
1594
|
+
static #queue = Promise.resolve();
|
|
1595
|
+
static pathIn(workspaceRoot) {
|
|
1596
|
+
return path7.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
|
|
1498
1597
|
}
|
|
1499
|
-
static async
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1598
|
+
static async run(workspaceRoot, label, fn) {
|
|
1599
|
+
const ahead = CodegenLock.#queue;
|
|
1600
|
+
let done;
|
|
1601
|
+
CodegenLock.#queue = new Promise((resolve) => {
|
|
1602
|
+
done = resolve;
|
|
1603
|
+
});
|
|
1604
|
+
try {
|
|
1605
|
+
await CodegenLock.#waitForQueue(ahead, label);
|
|
1606
|
+
return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
|
|
1607
|
+
} finally {
|
|
1608
|
+
done();
|
|
1609
|
+
}
|
|
1508
1610
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
};
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
class AkanWorkspaceConfig {
|
|
1522
|
-
static fileName = "akan.config.ts";
|
|
1523
|
-
static #keys = ["subspace", "pipeline", "cloud"];
|
|
1524
|
-
static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
|
|
1525
|
-
static async from(workspace, { bustImportCache = false } = {}) {
|
|
1526
|
-
const file = path7.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
|
|
1527
|
-
if (!await FileSys2.fileExists(file))
|
|
1528
|
-
return new AkanWorkspaceConfig(null, {});
|
|
1529
|
-
const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
|
|
1530
|
-
return new AkanWorkspaceConfig(file, input);
|
|
1611
|
+
static async#waitForQueue(ahead, label) {
|
|
1612
|
+
let timer = null;
|
|
1613
|
+
const expired = new Promise((resolve) => {
|
|
1614
|
+
timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
|
|
1615
|
+
});
|
|
1616
|
+
try {
|
|
1617
|
+
if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
|
|
1618
|
+
CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
|
|
1619
|
+
} finally {
|
|
1620
|
+
if (timer)
|
|
1621
|
+
clearTimeout(timer);
|
|
1622
|
+
}
|
|
1531
1623
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
if (
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
|
|
1624
|
+
static async#withFileLock(workspaceRoot, label, fn) {
|
|
1625
|
+
const lockPath = CodegenLock.pathIn(workspaceRoot);
|
|
1626
|
+
await mkdir(path7.dirname(lockPath), { recursive: true }).catch(() => {
|
|
1627
|
+
return;
|
|
1628
|
+
});
|
|
1629
|
+
const held = await CodegenLock.#acquire(lockPath, label);
|
|
1630
|
+
try {
|
|
1631
|
+
return await fn();
|
|
1632
|
+
} finally {
|
|
1633
|
+
if (held)
|
|
1634
|
+
await rm2(lockPath, { force: true }).catch(() => {
|
|
1635
|
+
return;
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1547
1638
|
}
|
|
1548
|
-
static #
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1639
|
+
static async#acquire(lockPath, label) {
|
|
1640
|
+
const deadline = Date.now() + CodegenLock.waitTimeoutMs;
|
|
1641
|
+
for (;; ) {
|
|
1642
|
+
const handle = await open(lockPath, "wx").catch(() => null);
|
|
1643
|
+
if (handle) {
|
|
1644
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
|
|
1645
|
+
return;
|
|
1646
|
+
});
|
|
1647
|
+
await handle.close().catch(() => {
|
|
1648
|
+
return;
|
|
1649
|
+
});
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
if (await CodegenLock.#reclaimIfAbandoned(lockPath))
|
|
1653
|
+
continue;
|
|
1654
|
+
if (Date.now() >= deadline) {
|
|
1655
|
+
CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
await Bun.sleep(CodegenLock.#pollMs);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
static async#reclaimIfAbandoned(lockPath) {
|
|
1662
|
+
const info = await stat(lockPath).catch(() => null);
|
|
1663
|
+
if (!info)
|
|
1664
|
+
return true;
|
|
1665
|
+
const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
|
|
1666
|
+
if (holder) {
|
|
1667
|
+
if (CodegenLock.#isAlive(holder.pid))
|
|
1668
|
+
return false;
|
|
1669
|
+
} else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
|
|
1670
|
+
return false;
|
|
1671
|
+
await rm2(lockPath, { force: true }).catch(() => {
|
|
1672
|
+
return;
|
|
1673
|
+
});
|
|
1674
|
+
return true;
|
|
1675
|
+
}
|
|
1676
|
+
static #parseHolder(raw) {
|
|
1677
|
+
try {
|
|
1678
|
+
const parsed = JSON.parse(raw);
|
|
1679
|
+
if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
|
|
1680
|
+
return null;
|
|
1681
|
+
return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
|
|
1682
|
+
} catch {
|
|
1561
1683
|
return null;
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (
|
|
1566
|
-
return
|
|
1567
|
-
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
static #isAlive(pid) {
|
|
1687
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
1688
|
+
return false;
|
|
1689
|
+
try {
|
|
1690
|
+
process.kill(pid, 0);
|
|
1691
|
+
return true;
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
return error.code === "EPERM";
|
|
1694
|
+
}
|
|
1568
1695
|
}
|
|
1569
1696
|
}
|
|
1697
|
+
|
|
1570
1698
|
// pkgs/@akanjs/devkit/jsonFormatter.ts
|
|
1571
1699
|
import path8 from "path";
|
|
1572
1700
|
|
|
@@ -1644,7 +1772,7 @@ ${" ".repeat(depth)}]`;
|
|
|
1644
1772
|
|
|
1645
1773
|
// pkgs/@akanjs/devkit/linter.ts
|
|
1646
1774
|
import { spawn } from "child_process";
|
|
1647
|
-
import { existsSync, readFileSync } from "fs";
|
|
1775
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1648
1776
|
import path9 from "path";
|
|
1649
1777
|
import chalk from "chalk";
|
|
1650
1778
|
var parseBiomeReport = (output) => {
|
|
@@ -1713,12 +1841,12 @@ class Linter {
|
|
|
1713
1841
|
this.lintRoot = this.#findBiomeRootPath(cwdPath);
|
|
1714
1842
|
this.configPath = Linter.#configPathIn(this.lintRoot) ?? path9.join(this.lintRoot, "biome.json");
|
|
1715
1843
|
const localBiomeBin = path9.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
|
|
1716
|
-
this.#biomeBin =
|
|
1844
|
+
this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
|
|
1717
1845
|
}
|
|
1718
1846
|
static #configPathIn(dir) {
|
|
1719
1847
|
for (const fileName of BIOME_CONFIG_FILES) {
|
|
1720
1848
|
const configPath = path9.join(dir, fileName);
|
|
1721
|
-
if (
|
|
1849
|
+
if (existsSync2(configPath))
|
|
1722
1850
|
return configPath;
|
|
1723
1851
|
}
|
|
1724
1852
|
return null;
|
|
@@ -1806,7 +1934,7 @@ class Linter {
|
|
|
1806
1934
|
};
|
|
1807
1935
|
}
|
|
1808
1936
|
async#checkFile(filePath, { write = false } = {}) {
|
|
1809
|
-
const originalContent =
|
|
1937
|
+
const originalContent = existsSync2(filePath) ? readFileSync(filePath, "utf8") : "";
|
|
1810
1938
|
const { stdout, stderr } = await this.#runBiome([
|
|
1811
1939
|
"check",
|
|
1812
1940
|
...write ? ["--write"] : [],
|
|
@@ -1820,7 +1948,7 @@ class Linter {
|
|
|
1820
1948
|
const report = parseBiomeReport(stdout || stderr);
|
|
1821
1949
|
const results = this.#toLintResults(report, filePath);
|
|
1822
1950
|
const { errors, warnings } = this.#splitMessages(results);
|
|
1823
|
-
const output = write &&
|
|
1951
|
+
const output = write && existsSync2(filePath) ? readFileSync(filePath, "utf8") : undefined;
|
|
1824
1952
|
return {
|
|
1825
1953
|
fixed: write && output !== originalContent,
|
|
1826
1954
|
output,
|
|
@@ -1836,7 +1964,7 @@ class Linter {
|
|
|
1836
1964
|
}
|
|
1837
1965
|
async lintFile(filePath) {
|
|
1838
1966
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
1839
|
-
if (!
|
|
1967
|
+
if (!existsSync2(resolvedFilePath))
|
|
1840
1968
|
throw new Error(`File not found: ${filePath}`);
|
|
1841
1969
|
return await this.#checkFile(resolvedFilePath);
|
|
1842
1970
|
}
|
|
@@ -1853,7 +1981,7 @@ class Linter {
|
|
|
1853
1981
|
output.push(`
|
|
1854
1982
|
${chalk.cyan(result.filePath)}`);
|
|
1855
1983
|
let sourceLines = [];
|
|
1856
|
-
if (
|
|
1984
|
+
if (existsSync2(result.filePath)) {
|
|
1857
1985
|
try {
|
|
1858
1986
|
const sourceContent = readFileSync(result.filePath, "utf8");
|
|
1859
1987
|
sourceLines = sourceContent.split(`
|
|
@@ -1925,7 +2053,7 @@ ${errorText}, ${warningText} found`];
|
|
|
1925
2053
|
}
|
|
1926
2054
|
async fixFile(filePath, dryRun = false) {
|
|
1927
2055
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
1928
|
-
if (!
|
|
2056
|
+
if (!existsSync2(resolvedFilePath))
|
|
1929
2057
|
throw new Error(`File not found: ${filePath}`);
|
|
1930
2058
|
if (!dryRun)
|
|
1931
2059
|
return await this.#checkFile(resolvedFilePath, { write: true });
|
|
@@ -1935,7 +2063,7 @@ ${errorText}, ${warningText} found`];
|
|
|
1935
2063
|
return { ...lintResult, fixed: stdout !== source, output: stdout };
|
|
1936
2064
|
}
|
|
1937
2065
|
async fixFiles(filePaths) {
|
|
1938
|
-
const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(
|
|
2066
|
+
const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync2);
|
|
1939
2067
|
if (resolved.length === 0)
|
|
1940
2068
|
return { fixed: [] };
|
|
1941
2069
|
const before = new Map(resolved.map((filePath) => [filePath, readFileSync(filePath, "utf8")]));
|
|
@@ -1955,7 +2083,7 @@ ${errorText}, ${warningText} found`];
|
|
|
1955
2083
|
}
|
|
1956
2084
|
async getConfigForFile(filePath) {
|
|
1957
2085
|
const resolvedFilePath = this.#resolveFilePath(filePath);
|
|
1958
|
-
if (!
|
|
2086
|
+
if (!existsSync2(resolvedFilePath))
|
|
1959
2087
|
throw new Error(`File not found: ${filePath}`);
|
|
1960
2088
|
return JSON.parse(readFileSync(this.configPath, "utf8"));
|
|
1961
2089
|
}
|
|
@@ -2244,7 +2372,7 @@ var moduleUiFileTypes = {
|
|
|
2244
2372
|
service: new Set(["Util", "Zone"]),
|
|
2245
2373
|
scalar: new Set(["Template", "Unit"])
|
|
2246
2374
|
};
|
|
2247
|
-
var createDependencyScanner = async (exec) => (await import("./dependencyScanner-
|
|
2375
|
+
var createDependencyScanner = async (exec) => (await import("./dependencyScanner-3afj2vt1.js")).TypeScriptDependencyScanner.from(exec);
|
|
2248
2376
|
var getScanPath = (exec, relativePath) => path11.posix.join(`${exec.type}s`, exec.name, relativePath.split(path11.sep).join("/"));
|
|
2249
2377
|
var getModuleNameFromPath = (kind, modulePath) => {
|
|
2250
2378
|
const dirname = path11.basename(modulePath);
|
|
@@ -2289,7 +2417,7 @@ async function assertScanConvention(exec, libRoot) {
|
|
|
2289
2417
|
]);
|
|
2290
2418
|
if (violations.length > 0) {
|
|
2291
2419
|
throw new Error(`[scan-convention]
|
|
2292
|
-
${violations.sort().map((violation) => `- ${violation}`).join(`
|
|
2420
|
+
${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
|
|
2293
2421
|
`)}`);
|
|
2294
2422
|
}
|
|
2295
2423
|
}
|
|
@@ -2613,6 +2741,136 @@ class WorkspaceInfo {
|
|
|
2613
2741
|
}
|
|
2614
2742
|
}
|
|
2615
2743
|
|
|
2744
|
+
// pkgs/@akanjs/devkit/spinner.ts
|
|
2745
|
+
import ora from "ora";
|
|
2746
|
+
|
|
2747
|
+
class Spinner {
|
|
2748
|
+
static padding = 12;
|
|
2749
|
+
static oraOptions = { discardStdin: false };
|
|
2750
|
+
static canAnimate(stream = process.stderr) {
|
|
2751
|
+
return !stream.isTTY || stream.columns > 0;
|
|
2752
|
+
}
|
|
2753
|
+
spinner;
|
|
2754
|
+
stopWatch = null;
|
|
2755
|
+
startAt = new Date;
|
|
2756
|
+
prefix;
|
|
2757
|
+
message;
|
|
2758
|
+
enableSpin;
|
|
2759
|
+
constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
|
|
2760
|
+
Spinner.padding = Math.max(Spinner.padding, prefix.length);
|
|
2761
|
+
this.prefix = prefix;
|
|
2762
|
+
this.message = message;
|
|
2763
|
+
this.spinner = ora({ ...Spinner.oraOptions, text: message });
|
|
2764
|
+
this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
|
|
2765
|
+
this.spinner.indent = indent;
|
|
2766
|
+
this.enableSpin = enableSpin && Spinner.canAnimate();
|
|
2767
|
+
}
|
|
2768
|
+
start() {
|
|
2769
|
+
this.startAt = new Date;
|
|
2770
|
+
if (this.enableSpin) {
|
|
2771
|
+
this.spinner.start();
|
|
2772
|
+
this.stopWatch = setInterval(() => {
|
|
2773
|
+
this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
|
|
2774
|
+
this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
|
|
2775
|
+
}, 1000);
|
|
2776
|
+
} else
|
|
2777
|
+
this.spinner.info();
|
|
2778
|
+
return this;
|
|
2779
|
+
}
|
|
2780
|
+
succeed(message) {
|
|
2781
|
+
this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
|
|
2782
|
+
this.#reset();
|
|
2783
|
+
}
|
|
2784
|
+
fail(message) {
|
|
2785
|
+
this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
|
|
2786
|
+
this.#reset();
|
|
2787
|
+
}
|
|
2788
|
+
isSpinning() {
|
|
2789
|
+
return this.spinner.isSpinning;
|
|
2790
|
+
}
|
|
2791
|
+
#reset() {
|
|
2792
|
+
if (this.stopWatch)
|
|
2793
|
+
clearInterval(this.stopWatch);
|
|
2794
|
+
this.stopWatch = null;
|
|
2795
|
+
}
|
|
2796
|
+
#getElapsedTimeStr({ floor = false } = {}) {
|
|
2797
|
+
const ms = Date.now() - this.startAt.getTime();
|
|
2798
|
+
if (ms < 1000)
|
|
2799
|
+
return `${ms}ms`;
|
|
2800
|
+
const s = Math.floor(ms / 1000);
|
|
2801
|
+
if (s < 60)
|
|
2802
|
+
return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
|
|
2803
|
+
const m = Math.floor(s / 60);
|
|
2804
|
+
return `${m}m ${s % 60}s`;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// pkgs/@akanjs/devkit/typecheck/projectTypecheck.ts
|
|
2809
|
+
import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
|
|
2810
|
+
import path12 from "path";
|
|
2811
|
+
|
|
2812
|
+
class ProjectTypecheck2 {
|
|
2813
|
+
projectDir;
|
|
2814
|
+
workspaceRoot;
|
|
2815
|
+
env;
|
|
2816
|
+
static #testIncludes = ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"];
|
|
2817
|
+
static #excludes = [".akan/**", "node_modules/**", "local/**"];
|
|
2818
|
+
constructor(projectDir, workspaceRoot, env) {
|
|
2819
|
+
this.projectDir = projectDir;
|
|
2820
|
+
this.workspaceRoot = workspaceRoot;
|
|
2821
|
+
this.env = env;
|
|
2822
|
+
}
|
|
2823
|
+
get typecheckDir() {
|
|
2824
|
+
return path12.join(this.projectDir, ".akan", "typecheck");
|
|
2825
|
+
}
|
|
2826
|
+
async run(entries, { clean = false, incremental = true, tests = false } = {}) {
|
|
2827
|
+
const name = tests ? "tsconfig.tests" : "tsconfig";
|
|
2828
|
+
await mkdir2(this.typecheckDir, { recursive: true });
|
|
2829
|
+
const tsconfig = {
|
|
2830
|
+
extends: "../../tsconfig.json",
|
|
2831
|
+
compilerOptions: { noEmit: true, incremental, tsBuildInfoFile: `./${name}.tsbuildinfo` },
|
|
2832
|
+
include: [...entries, ...tests ? ProjectTypecheck2.#testIncludes.map((glob) => `../../${glob}`) : []],
|
|
2833
|
+
...tests ? { exclude: ProjectTypecheck2.#excludes.map((glob) => `../../${glob}`) } : {},
|
|
2834
|
+
references: []
|
|
2835
|
+
};
|
|
2836
|
+
const tsconfigPath = path12.join(this.typecheckDir, `${name}.json`);
|
|
2837
|
+
await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
|
|
2838
|
+
`);
|
|
2839
|
+
if (clean)
|
|
2840
|
+
await rm3(path12.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
|
|
2841
|
+
await this.#check(tsconfigPath);
|
|
2842
|
+
return tsconfigPath;
|
|
2843
|
+
}
|
|
2844
|
+
async#check(tsconfigPath) {
|
|
2845
|
+
const proc = Bun.spawn([process.execPath, await ProjectTypecheck2.workerEntry(this.workspaceRoot)], {
|
|
2846
|
+
cwd: this.workspaceRoot,
|
|
2847
|
+
env: { ...this.env, AKAN_COMMAND_TYPE: "typecheck", AKAN_TYPECHECK_TSCONFIG: tsconfigPath },
|
|
2848
|
+
stdout: "pipe",
|
|
2849
|
+
stderr: "pipe"
|
|
2850
|
+
});
|
|
2851
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2852
|
+
new Response(proc.stdout).text(),
|
|
2853
|
+
new Response(proc.stderr).text(),
|
|
2854
|
+
proc.exited
|
|
2855
|
+
]);
|
|
2856
|
+
if (exitCode !== 0)
|
|
2857
|
+
throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
2858
|
+
}
|
|
2859
|
+
static async workerEntry(workspaceRoot) {
|
|
2860
|
+
const candidates = [
|
|
2861
|
+
path12.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
2862
|
+
path12.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
2863
|
+
path12.join(import.meta.dir, "typecheck.proc.js"),
|
|
2864
|
+
path12.join(import.meta.dir, "typecheck.proc.ts"),
|
|
2865
|
+
path12.join(import.meta.dir, "..", "typecheck.proc.js")
|
|
2866
|
+
];
|
|
2867
|
+
for (const candidate of candidates)
|
|
2868
|
+
if (await Bun.file(candidate).exists())
|
|
2869
|
+
return candidate;
|
|
2870
|
+
throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2616
2874
|
// pkgs/@akanjs/devkit/executors.ts
|
|
2617
2875
|
var staticTemplateFileExtensions = new Set(".avif .bmp .cjs .css .eot .gif .html .ico .jpeg .jpg .js .json .map .md .mjs .mp3 .mp4 .ogg .otf .pdf .png .svg".split(" ").concat(".ttf .txt .wasm .wav .webm .webp .woff .woff2 .xml".split(" ")));
|
|
2618
2876
|
var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
|
|
@@ -2709,16 +2967,16 @@ class Executor {
|
|
|
2709
2967
|
linter = null;
|
|
2710
2968
|
constructor(name, cwdPath) {
|
|
2711
2969
|
this.name = name;
|
|
2712
|
-
this.logger = new
|
|
2970
|
+
this.logger = new Logger3(name);
|
|
2713
2971
|
this.logs = [];
|
|
2714
2972
|
this.cwdPath = cwdPath;
|
|
2715
2973
|
}
|
|
2716
2974
|
#stdout(data) {
|
|
2717
2975
|
if (Executor.verbose)
|
|
2718
|
-
|
|
2976
|
+
Logger3.raw(chalk2.dim(data.toString()));
|
|
2719
2977
|
}
|
|
2720
2978
|
#stderr(data) {
|
|
2721
|
-
|
|
2979
|
+
Logger3.raw(chalk2.red(data.toString()));
|
|
2722
2980
|
}
|
|
2723
2981
|
#settle(proc, target, { onClose = false, keepLogs = false, redStderr = false } = {}) {
|
|
2724
2982
|
let stdout = "";
|
|
@@ -2767,10 +3025,10 @@ class Executor {
|
|
|
2767
3025
|
return this.#settle(proc, target, { redStderr: true }).then(({ code, signal }) => ({ code, signal }));
|
|
2768
3026
|
}
|
|
2769
3027
|
getPath(filePath) {
|
|
2770
|
-
if (
|
|
3028
|
+
if (path13.isAbsolute(filePath))
|
|
2771
3029
|
return filePath;
|
|
2772
3030
|
if (filePath.startsWith("."))
|
|
2773
|
-
return
|
|
3031
|
+
return path13.join(this.cwdPath, filePath);
|
|
2774
3032
|
const baseParts = this.cwdPath.split(/[\\/]/).filter(Boolean);
|
|
2775
3033
|
const targetParts = filePath.split(/[\\/]/).filter(Boolean);
|
|
2776
3034
|
let overlapLength = 0;
|
|
@@ -2784,12 +3042,12 @@ class Executor {
|
|
|
2784
3042
|
if (isOverlap)
|
|
2785
3043
|
overlapLength = i;
|
|
2786
3044
|
}
|
|
2787
|
-
return
|
|
3045
|
+
return path13.join(this.cwdPath, ...targetParts.slice(overlapLength));
|
|
2788
3046
|
}
|
|
2789
3047
|
async mkdir(dirPath) {
|
|
2790
3048
|
const writePath = this.getPath(dirPath);
|
|
2791
3049
|
if (!await FileSys2.dirExists(writePath))
|
|
2792
|
-
await
|
|
3050
|
+
await mkdir3(writePath, { recursive: true });
|
|
2793
3051
|
this.logger.verbose(`Make directory ${writePath}`);
|
|
2794
3052
|
return this;
|
|
2795
3053
|
}
|
|
@@ -2797,20 +3055,20 @@ class Executor {
|
|
|
2797
3055
|
const readPath = this.getPath(dirPath);
|
|
2798
3056
|
try {
|
|
2799
3057
|
const glob = new Bun.Glob("*");
|
|
2800
|
-
return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false }));
|
|
3058
|
+
return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false })).sort();
|
|
2801
3059
|
} catch {
|
|
2802
3060
|
return [];
|
|
2803
3061
|
}
|
|
2804
3062
|
}
|
|
2805
3063
|
async getAllFiles(pattern = "**/*", { cwd } = {}) {
|
|
2806
3064
|
const glob = new Bun.Glob(pattern);
|
|
2807
|
-
return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true }));
|
|
3065
|
+
return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true })).sort();
|
|
2808
3066
|
}
|
|
2809
3067
|
async getFilesAndDirs(dirPath) {
|
|
2810
3068
|
const fullDirPath = this.getPath(dirPath);
|
|
2811
3069
|
const glob = new Bun.Glob("*");
|
|
2812
|
-
const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
|
|
2813
|
-
const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
|
|
3070
|
+
const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true })).sort();
|
|
3071
|
+
const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false })).sort();
|
|
2814
3072
|
const dirs = allEntries.filter((entry) => !files.includes(entry));
|
|
2815
3073
|
return { files, dirs };
|
|
2816
3074
|
}
|
|
@@ -2825,26 +3083,28 @@ class Executor {
|
|
|
2825
3083
|
return this;
|
|
2826
3084
|
}
|
|
2827
3085
|
async removeDir(dirPath) {
|
|
2828
|
-
const readPath =
|
|
3086
|
+
const readPath = path13.join(this.getPath(dirPath), ".");
|
|
2829
3087
|
if (await FileSys2.entryExists(readPath))
|
|
2830
|
-
await
|
|
3088
|
+
await rm4(readPath, { recursive: true, force: true });
|
|
2831
3089
|
this.logger.verbose(`Remove directory ${readPath}`);
|
|
2832
3090
|
return this;
|
|
2833
3091
|
}
|
|
2834
3092
|
async writeFile(filePath, content, { overwrite = true, silent = false } = {}) {
|
|
2835
3093
|
const writePath = this.getPath(filePath);
|
|
2836
|
-
const dir =
|
|
3094
|
+
const dir = path13.dirname(writePath);
|
|
2837
3095
|
if (!await FileSys2.dirExists(dir))
|
|
2838
|
-
await
|
|
3096
|
+
await mkdir3(dir, { recursive: true });
|
|
2839
3097
|
let contentStr = typeof content === "string" ? content : JsonFormatter.stringify(content, writePath);
|
|
3098
|
+
let written = true;
|
|
2840
3099
|
if (await FileSys2.fileExists(writePath)) {
|
|
2841
3100
|
const currentContent = await FileSys2.readText(writePath);
|
|
2842
3101
|
if (currentContent === contentStr || !overwrite) {
|
|
2843
3102
|
this.logger.verbose(`File ${writePath} is unchanged`);
|
|
2844
3103
|
contentStr = currentContent;
|
|
3104
|
+
written = false;
|
|
2845
3105
|
} else {
|
|
2846
3106
|
await FileSys2.writeText(writePath, contentStr);
|
|
2847
|
-
if (
|
|
3107
|
+
if (Logger3.isVerbose())
|
|
2848
3108
|
this.logger.rawLog(chalk2.yellow(`File Update: ${filePath}`));
|
|
2849
3109
|
}
|
|
2850
3110
|
} else {
|
|
@@ -2852,13 +3112,13 @@ class Executor {
|
|
|
2852
3112
|
if (!silent)
|
|
2853
3113
|
this.logger.rawLog(chalk2.green(`File Create: ${filePath}`));
|
|
2854
3114
|
}
|
|
2855
|
-
return { filePath: writePath, content: contentStr };
|
|
3115
|
+
return { filePath: writePath, content: contentStr, written };
|
|
2856
3116
|
}
|
|
2857
3117
|
async writeJson(filePath, content) {
|
|
2858
3118
|
await this.writeFile(filePath, content);
|
|
2859
3119
|
}
|
|
2860
3120
|
async getLocalFile(targetPath) {
|
|
2861
|
-
const filePath =
|
|
3121
|
+
const filePath = path13.isAbsolute(targetPath) ? targetPath : targetPath.replace(this.cwdPath, "");
|
|
2862
3122
|
const content = await this.readFile(filePath);
|
|
2863
3123
|
return { filePath, content };
|
|
2864
3124
|
}
|
|
@@ -2875,7 +3135,7 @@ class Executor {
|
|
|
2875
3135
|
return;
|
|
2876
3136
|
const isDirectory = (await stat2(src)).isDirectory();
|
|
2877
3137
|
if (!await FileSys2.exists(dest) && isDirectory)
|
|
2878
|
-
await
|
|
3138
|
+
await mkdir3(dest, { recursive: true });
|
|
2879
3139
|
if (dereference || process.platform === "win32")
|
|
2880
3140
|
await cpEntry(src, dest, { recursive: isDirectory, dereference: true, force: true });
|
|
2881
3141
|
else
|
|
@@ -2945,9 +3205,9 @@ class Executor {
|
|
|
2945
3205
|
const result = await getContent.default(scanInfo ?? null, dict, options);
|
|
2946
3206
|
if (result === null)
|
|
2947
3207
|
return null;
|
|
2948
|
-
const filename = typeof result === "object" ? result.filename :
|
|
3208
|
+
const filename = typeof result === "object" ? result.filename : path13.basename(targetPath).replace(".js", ".ts");
|
|
2949
3209
|
const content = typeof result === "object" ? result.content : result;
|
|
2950
|
-
const convertedTargetPath = convertPath(`${
|
|
3210
|
+
const convertedTargetPath = convertPath(`${path13.dirname(targetPath)}/${filename}`);
|
|
2951
3211
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
2952
3212
|
return this.writeFile(convertedTargetPath, content, { overwrite });
|
|
2953
3213
|
} else if (targetPath.endsWith(".template")) {
|
|
@@ -2956,15 +3216,17 @@ class Executor {
|
|
|
2956
3216
|
const convertedContent = Object.entries(dict).reduce((data, [key, value]) => data.replace(new RegExp(`<%= ${key} %>`, "g"), value), content);
|
|
2957
3217
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
2958
3218
|
return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
|
|
2959
|
-
} else if (staticTemplateFileExtensions.has(
|
|
3219
|
+
} else if (staticTemplateFileExtensions.has(path13.extname(targetPath).toLowerCase())) {
|
|
2960
3220
|
const convertedTargetPath = convertPath(targetPath);
|
|
2961
3221
|
const writePath = this.getPath(convertedTargetPath);
|
|
2962
|
-
|
|
3222
|
+
if (!overwrite && await FileSys2.fileExists(writePath))
|
|
3223
|
+
return { filePath: writePath, content: "", written: false };
|
|
3224
|
+
const dirname = path13.dirname(writePath);
|
|
2963
3225
|
if (!await FileSys2.dirExists(dirname))
|
|
2964
|
-
await
|
|
3226
|
+
await mkdir3(dirname, { recursive: true });
|
|
2965
3227
|
await copyFile(templatePath, writePath);
|
|
2966
3228
|
this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
|
|
2967
|
-
return { filePath: writePath, content: "" };
|
|
3229
|
+
return { filePath: writePath, content: "", written: true };
|
|
2968
3230
|
} else
|
|
2969
3231
|
return null;
|
|
2970
3232
|
}
|
|
@@ -2981,7 +3243,7 @@ class Executor {
|
|
|
2981
3243
|
if ((await stat2(templatePath)).isFile()) {
|
|
2982
3244
|
const fileContent = await this.#applyTemplateFile({
|
|
2983
3245
|
templatePath,
|
|
2984
|
-
targetPath:
|
|
3246
|
+
targetPath: path13.join(basePath, path13.basename(templatePath)),
|
|
2985
3247
|
scanInfo,
|
|
2986
3248
|
overwrite
|
|
2987
3249
|
}, dict, options);
|
|
@@ -2989,19 +3251,19 @@ class Executor {
|
|
|
2989
3251
|
} else {
|
|
2990
3252
|
const subdirs = await readDirEntries(templatePath);
|
|
2991
3253
|
const fileContents = (await Promise.all(subdirs.map(async (subdir) => {
|
|
2992
|
-
const subpath =
|
|
3254
|
+
const subpath = path13.join(templatePath, subdir);
|
|
2993
3255
|
if ((await stat2(subpath)).isFile()) {
|
|
2994
3256
|
const fileContent = await this.#applyTemplateFile({
|
|
2995
3257
|
templatePath: subpath,
|
|
2996
|
-
targetPath:
|
|
3258
|
+
targetPath: path13.join(basePath, subdir),
|
|
2997
3259
|
scanInfo,
|
|
2998
3260
|
overwrite
|
|
2999
3261
|
}, dict, options);
|
|
3000
3262
|
return fileContent ? [fileContent] : [];
|
|
3001
3263
|
} else
|
|
3002
3264
|
return await this._applyTemplate({
|
|
3003
|
-
basePath:
|
|
3004
|
-
template:
|
|
3265
|
+
basePath: path13.join(basePath, subdir),
|
|
3266
|
+
template: path13.join(template, subdir),
|
|
3005
3267
|
scanInfo,
|
|
3006
3268
|
dict,
|
|
3007
3269
|
overwrite,
|
|
@@ -3013,8 +3275,8 @@ class Executor {
|
|
|
3013
3275
|
}
|
|
3014
3276
|
async#resolveTemplateRoot() {
|
|
3015
3277
|
const candidates = [
|
|
3016
|
-
|
|
3017
|
-
|
|
3278
|
+
path13.resolve(getDirname2(import.meta.url), "templates"),
|
|
3279
|
+
path13.resolve(getDirname2(import.meta.url), "../cli/templates")
|
|
3018
3280
|
];
|
|
3019
3281
|
for (const candidate of candidates) {
|
|
3020
3282
|
if (await FileSys2.dirExists(candidate))
|
|
@@ -3024,7 +3286,7 @@ class Executor {
|
|
|
3024
3286
|
}
|
|
3025
3287
|
async applyTemplate(options) {
|
|
3026
3288
|
const fileContents = await this._applyTemplate({ ...options, dict: withCapitalizedDict(options.dict) });
|
|
3027
|
-
await this.#formatAppliedTemplate(fileContents);
|
|
3289
|
+
await this.#formatAppliedTemplate(fileContents.filter((fileContent) => fileContent.written));
|
|
3028
3290
|
return fileContents;
|
|
3029
3291
|
}
|
|
3030
3292
|
async#formatAppliedTemplate(fileContents) {
|
|
@@ -3036,7 +3298,7 @@ class Executor {
|
|
|
3036
3298
|
if (fixed.length > 0)
|
|
3037
3299
|
this.logger.verbose(`Formatted ${fixed.length} scaffolded file(s)`);
|
|
3038
3300
|
} catch (err) {
|
|
3039
|
-
this.logger.verbose(`Skipped formatting scaffolded files: ${err
|
|
3301
|
+
this.logger.verbose(`Skipped formatting scaffolded files: ${toError2(err).message}`);
|
|
3040
3302
|
}
|
|
3041
3303
|
}
|
|
3042
3304
|
async getTypeChecker() {
|
|
@@ -3053,7 +3315,7 @@ class Executor {
|
|
|
3053
3315
|
}
|
|
3054
3316
|
async typeCheckAsync(filePath) {
|
|
3055
3317
|
const path = this.getPath(filePath);
|
|
3056
|
-
const entry = await
|
|
3318
|
+
const entry = await ProjectTypecheck2.workerEntry(process.cwd());
|
|
3057
3319
|
const proc = Bun.spawn([process.execPath, entry], {
|
|
3058
3320
|
cwd: this.cwdPath,
|
|
3059
3321
|
env: {
|
|
@@ -3079,20 +3341,6 @@ class Executor {
|
|
|
3079
3341
|
message: result.message
|
|
3080
3342
|
};
|
|
3081
3343
|
}
|
|
3082
|
-
async#resolveTypecheckWorkerEntry() {
|
|
3083
|
-
const dirname = getDirname2(import.meta.url);
|
|
3084
|
-
const candidates = [
|
|
3085
|
-
path12.join(process.cwd(), "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
3086
|
-
path12.join(process.cwd(), "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
3087
|
-
path12.join(dirname, "typecheck/typecheck.proc.ts"),
|
|
3088
|
-
path12.join(dirname, "typecheck.proc.js"),
|
|
3089
|
-
path12.join(dirname, "typecheck.proc.ts")
|
|
3090
|
-
];
|
|
3091
|
-
for (const candidate of candidates)
|
|
3092
|
-
if (await Bun.file(candidate).exists())
|
|
3093
|
-
return candidate;
|
|
3094
|
-
throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
3095
|
-
}
|
|
3096
3344
|
getLinter() {
|
|
3097
3345
|
this.linter ??= new Linter(this.cwdPath);
|
|
3098
3346
|
return this.linter;
|
|
@@ -3124,17 +3372,19 @@ class WorkspaceExecutor extends Executor {
|
|
|
3124
3372
|
} = {}) {
|
|
3125
3373
|
return new WorkspaceExecutor({ workspaceRoot, repoName });
|
|
3126
3374
|
}
|
|
3375
|
+
static publicEnvPin = null;
|
|
3376
|
+
static localServeDomain = "localhost";
|
|
3127
3377
|
static getBaseDevEnv(envPath) {
|
|
3128
|
-
const sourceEnv =
|
|
3378
|
+
const sourceEnv = {
|
|
3379
|
+
...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
|
|
3380
|
+
...WorkspaceExecutor.publicEnvPin
|
|
3381
|
+
};
|
|
3129
3382
|
const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
|
|
3130
3383
|
const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
|
|
3131
3384
|
const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
|
|
3132
|
-
const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
3136
|
-
if (!serveDomain)
|
|
3137
|
-
throw new Error("AKAN_PUBLIC_SERVE_DOMAIN is not set");
|
|
3385
|
+
const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME || resolveRepoName(envPath ? path13.dirname(envPath) : workspaceRoot ?? process.cwd());
|
|
3386
|
+
const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN || WorkspaceExecutor.localServeDomain;
|
|
3387
|
+
const defaulted = ["AKAN_PUBLIC_REPO_NAME", "AKAN_PUBLIC_SERVE_DOMAIN"].filter((key) => !sourceEnv[key]);
|
|
3138
3388
|
const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
|
|
3139
3389
|
const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
|
|
3140
3390
|
if (!env)
|
|
@@ -3146,9 +3396,18 @@ class WorkspaceExecutor extends Executor {
|
|
|
3146
3396
|
serveDomain,
|
|
3147
3397
|
env,
|
|
3148
3398
|
portOffset,
|
|
3149
|
-
workspaceId
|
|
3399
|
+
workspaceId,
|
|
3400
|
+
defaulted
|
|
3150
3401
|
};
|
|
3151
3402
|
}
|
|
3403
|
+
static assertDeployableEnv(baseDevEnv, environment, purpose) {
|
|
3404
|
+
const defaulted = environment === "local" ? [] : baseDevEnv.defaulted ?? [];
|
|
3405
|
+
if (!defaulted.length)
|
|
3406
|
+
return;
|
|
3407
|
+
const values = { AKAN_PUBLIC_REPO_NAME: baseDevEnv.repoName, AKAN_PUBLIC_SERVE_DOMAIN: baseDevEnv.serveDomain };
|
|
3408
|
+
const standIns = defaulted.map((key) => `${key} (would be "${values[key]}")`).join(" and ");
|
|
3409
|
+
throw new Error(`${purpose} for the "${environment}" environment needs ${standIns} set, not a local default. Set ${defaulted.length > 1 ? "them" : "it"} in the workspace .env or the shell, or use AKAN_PUBLIC_ENV=local.`);
|
|
3410
|
+
}
|
|
3152
3411
|
getWorkspaceId({
|
|
3153
3412
|
allowEmpty
|
|
3154
3413
|
} = {}) {
|
|
@@ -3213,7 +3472,7 @@ class WorkspaceExecutor extends Executor {
|
|
|
3213
3472
|
await Promise.all(dirs.map(async (dir) => {
|
|
3214
3473
|
if (dir.includes("_") || AVOID_DIRS.includes(dir))
|
|
3215
3474
|
return;
|
|
3216
|
-
const dirPath =
|
|
3475
|
+
const dirPath = path13.join(dirname, dir);
|
|
3217
3476
|
if ((await stat2(dirPath)).isDirectory()) {
|
|
3218
3477
|
results.push(`${prefix}${dir}`);
|
|
3219
3478
|
if (maxDepth > 0)
|
|
@@ -3250,9 +3509,9 @@ class WorkspaceExecutor extends Executor {
|
|
|
3250
3509
|
await Promise.all(dirs.map(async (dir) => {
|
|
3251
3510
|
if (AVOID_DIRS.includes(dir))
|
|
3252
3511
|
return;
|
|
3253
|
-
const dirPath =
|
|
3512
|
+
const dirPath = path13.join(dirname, dir);
|
|
3254
3513
|
if (await FileSys2.dirExists(dirPath)) {
|
|
3255
|
-
const hasTargetFile = await FileSys2.fileExists(
|
|
3514
|
+
const hasTargetFile = await FileSys2.fileExists(path13.join(dirPath, targetFilename));
|
|
3256
3515
|
if (hasTargetFile)
|
|
3257
3516
|
results.push(`${prefix}${dir}`);
|
|
3258
3517
|
if (maxDepth > 0)
|
|
@@ -3311,7 +3570,7 @@ class SysExecutor extends Executor {
|
|
|
3311
3570
|
return [];
|
|
3312
3571
|
const violations = [];
|
|
3313
3572
|
for (const entry of await this.getAllFiles("**/*", { cwd: this.getPath("page") })) {
|
|
3314
|
-
const relativePath = entry.split(
|
|
3573
|
+
const relativePath = entry.split(path13.sep).join("/");
|
|
3315
3574
|
const reason = getPageSourceFileViolation(relativePath);
|
|
3316
3575
|
if (reason)
|
|
3317
3576
|
violations.push({ relativePath: `page/${relativePath}`, reason });
|
|
@@ -3406,21 +3665,21 @@ class SysExecutor extends Executor {
|
|
|
3406
3665
|
await this.setPackageJson({ ...packageJson, dependencies, devDependencies });
|
|
3407
3666
|
}
|
|
3408
3667
|
async getLocalFile(targetPath) {
|
|
3409
|
-
const filePath =
|
|
3668
|
+
const filePath = path13.isAbsolute(targetPath) ? targetPath : `${this.type}s/${this.name}/${targetPath}`;
|
|
3410
3669
|
const content = await this.workspace.readFile(filePath);
|
|
3411
3670
|
return { filePath, content };
|
|
3412
3671
|
}
|
|
3413
3672
|
async getDatabaseModules() {
|
|
3414
|
-
return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) =>
|
|
3673
|
+
return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
|
|
3415
3674
|
}
|
|
3416
3675
|
async getServiceModules() {
|
|
3417
|
-
return (await this.readdir("lib")).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) =>
|
|
3676
|
+
return (await this.readdir("lib")).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${name.slice(1)}.service.ts`));
|
|
3418
3677
|
}
|
|
3419
3678
|
async getScalarModules() {
|
|
3420
|
-
return (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_")).filter((name) =>
|
|
3679
|
+
return (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_")).filter((name) => existsSync3(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
|
|
3421
3680
|
}
|
|
3422
3681
|
async#getComponentModules(role) {
|
|
3423
|
-
return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) =>
|
|
3682
|
+
return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${capitalize(name)}.${role}.tsx`));
|
|
3424
3683
|
}
|
|
3425
3684
|
async#getComponentSources(role) {
|
|
3426
3685
|
const modules = await this.#getComponentModules(role);
|
|
@@ -3494,19 +3753,19 @@ class AppExecutor extends SysExecutor {
|
|
|
3494
3753
|
static from(executor, name) {
|
|
3495
3754
|
return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
|
|
3496
3755
|
}
|
|
3497
|
-
static #keptOnStart = [
|
|
3498
|
-
static #legacyNative =
|
|
3756
|
+
static #keptOnStart = [path13.join(".akan", "cache", "bin")];
|
|
3757
|
+
static #legacyNative = path13.join(".akan", "native");
|
|
3499
3758
|
async#removeDevOutput(dir = ".akan") {
|
|
3500
3759
|
const entries = await readDirEntries(this.getPath(dir)).catch(() => []);
|
|
3501
3760
|
await Promise.all(entries.map(async (name) => {
|
|
3502
|
-
const entry =
|
|
3761
|
+
const entry = path13.join(dir, name);
|
|
3503
3762
|
if (entry === AppExecutor.#legacyNative) {
|
|
3504
3763
|
this.logger.warn(`apps/${this.name}/.akan/native holds native builds from before they moved to dist/native/${this.name}: upload any release waiting in its updates folders, then delete it.`);
|
|
3505
3764
|
return;
|
|
3506
3765
|
}
|
|
3507
3766
|
if (AppExecutor.#keptOnStart.includes(entry))
|
|
3508
3767
|
return;
|
|
3509
|
-
if (AppExecutor.#keptOnStart.some((kept) => kept.startsWith(`${entry}${
|
|
3768
|
+
if (AppExecutor.#keptOnStart.some((kept) => kept.startsWith(`${entry}${path13.sep}`)))
|
|
3510
3769
|
return await this.#removeDevOutput(entry);
|
|
3511
3770
|
await this.removeDir(entry);
|
|
3512
3771
|
}));
|
|
@@ -3525,10 +3784,12 @@ class AppExecutor extends SysExecutor {
|
|
|
3525
3784
|
return basePort + appIndex + portOffset;
|
|
3526
3785
|
}
|
|
3527
3786
|
getCommandEnv(env = {}) {
|
|
3528
|
-
const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
|
|
3787
|
+
const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
|
|
3529
3788
|
const PORT = (8282 + portOffset).toString();
|
|
3530
3789
|
return {
|
|
3531
3790
|
...process.env,
|
|
3791
|
+
AKAN_PUBLIC_REPO_NAME: repoName,
|
|
3792
|
+
AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
|
|
3532
3793
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
3533
3794
|
AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
|
|
3534
3795
|
NODE_NO_WARNINGS: "1",
|
|
@@ -3630,13 +3891,13 @@ class AppExecutor extends SysExecutor {
|
|
|
3630
3891
|
absolute: false,
|
|
3631
3892
|
onlyFiles: true
|
|
3632
3893
|
})) {
|
|
3633
|
-
const segments = rel.split(
|
|
3894
|
+
const segments = rel.split(path13.sep);
|
|
3634
3895
|
if (segments.some((s) => s === "node_modules"))
|
|
3635
3896
|
continue;
|
|
3636
3897
|
const posix = `${root.keyPrefix}${segments.join("/")}`;
|
|
3637
3898
|
if (!root.keyPrefix && libKeyPrefixes.some((prefix) => posix.startsWith(prefix)))
|
|
3638
3899
|
continue;
|
|
3639
|
-
const absPath =
|
|
3900
|
+
const absPath = path13.join(root.dir, ...segments);
|
|
3640
3901
|
validatePageSourceFile(posix, { filePath: absPath });
|
|
3641
3902
|
if (!isRouteSourceFile(posix))
|
|
3642
3903
|
continue;
|
|
@@ -3679,7 +3940,7 @@ class AppExecutor extends SysExecutor {
|
|
|
3679
3940
|
pageKeys.push(key);
|
|
3680
3941
|
}
|
|
3681
3942
|
}
|
|
3682
|
-
pageKeys.sort();
|
|
3943
|
+
pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3683
3944
|
this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
|
|
3684
3945
|
return this.#pageKeys;
|
|
3685
3946
|
}
|
|
@@ -3777,12 +4038,12 @@ class AppExecutor extends SysExecutor {
|
|
|
3777
4038
|
static async#linkLibAsset(targetPath, linkPath) {
|
|
3778
4039
|
const isWindows = process.platform === "win32";
|
|
3779
4040
|
try {
|
|
3780
|
-
const target = isWindows ? targetPath :
|
|
4041
|
+
const target = isWindows ? targetPath : path13.relative(path13.dirname(linkPath), targetPath);
|
|
3781
4042
|
await symlink(target, linkPath, isWindows ? "junction" : "dir");
|
|
3782
4043
|
} catch (error) {
|
|
3783
4044
|
if (!isWindows)
|
|
3784
4045
|
throw error;
|
|
3785
|
-
await
|
|
4046
|
+
await mkdir3(linkPath, { recursive: true });
|
|
3786
4047
|
await cpEntry(targetPath, linkPath, { recursive: true, dereference: true, force: true });
|
|
3787
4048
|
}
|
|
3788
4049
|
}
|
|
@@ -3820,7 +4081,7 @@ class AppExecutor extends SysExecutor {
|
|
|
3820
4081
|
await this.writeFile(rel, content, opts);
|
|
3821
4082
|
},
|
|
3822
4083
|
readEnvClient: async () => {
|
|
3823
|
-
const envClientPath =
|
|
4084
|
+
const envClientPath = path13.join(this.cwdPath, "env", "env.client.ts");
|
|
3824
4085
|
if (!await FileSys2.fileExists(envClientPath))
|
|
3825
4086
|
return null;
|
|
3826
4087
|
try {
|
|
@@ -3914,7 +4175,7 @@ class PkgExecutor extends Executor {
|
|
|
3914
4175
|
return overrideVersion;
|
|
3915
4176
|
try {
|
|
3916
4177
|
const packageJsonPath = `pkgs/${dep}/package.json`;
|
|
3917
|
-
if (!await Bun.file(
|
|
4178
|
+
if (!await Bun.file(path13.join(this.workspace.workspaceRoot, packageJsonPath)).exists())
|
|
3918
4179
|
return;
|
|
3919
4180
|
const packageJson = await this.workspace.readJson(packageJsonPath);
|
|
3920
4181
|
if (packageJson.name === dep)
|
|
@@ -3925,7 +4186,7 @@ class PkgExecutor extends Executor {
|
|
|
3925
4186
|
}
|
|
3926
4187
|
async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
|
|
3927
4188
|
const dependencyNames = [...new Set(dependencies)].sort();
|
|
3928
|
-
const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort();
|
|
4189
|
+
const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3929
4190
|
const dependencyVersions = new Map;
|
|
3930
4191
|
const missingDeps = [];
|
|
3931
4192
|
for (const dep of [...dependencyNames, ...devDependencyNames]) {
|
|
@@ -4007,4 +4268,4 @@ class ModuleExecutor extends Executor {
|
|
|
4007
4268
|
}
|
|
4008
4269
|
}
|
|
4009
4270
|
|
|
4010
|
-
export { AGENT_BLOCK_START, AGENT_BLOCK_END, stampBlockVersion, extractBlockVersion, readDevkitVersion, upsertAgentBlock, extractAgentBlock, renderRecipeEntries, collectScopeRecipeSources, renderScopeAgentBlock, CLAUDE_COMMENT_RULE, binPlatforms, ServerEnvProfile, AkanAppConfig, AkanWorkspaceConfig, AsyncDefaultExportDetector, resolveSsrPageEntriesForApp, CodegenLock, Linter, LibSource, formatLibStatuses, rootAllowedFiles, rootAllowedDirs, isAllowedLibFacetRootFile, libFacetRootHintOf, barrelFacetDirs, facetIndexShadowOf, rootEntryHintOf, isScannedRootEntry, AppInfo, Spinner, Executor, WorkspaceExecutor, SysExecutor, AppExecutor, LibExecutor, PkgExecutor, ModuleExecutor };
|
|
4271
|
+
export { AGENT_BLOCK_START, AGENT_BLOCK_END, stampBlockVersion, extractBlockVersion, readDevkitVersion, upsertAgentBlock, extractAgentBlock, renderRecipeEntries, collectScopeRecipeSources, renderScopeAgentBlock, CLAUDE_COMMENT_RULE, binPlatforms, ServerEnvProfile, AkanAppConfig, AkanWorkspaceConfig, AsyncDefaultExportDetector, resolveSsrPageEntriesForApp, CodegenLock, Linter, LibSource, formatLibStatuses, rootAllowedFiles, rootAllowedDirs, isAllowedLibFacetRootFile, libFacetRootHintOf, barrelFacetDirs, facetIndexShadowOf, rootEntryHintOf, isScannedRootEntry, AppInfo, Spinner, ProjectTypecheck2, Executor, WorkspaceExecutor, SysExecutor, AppExecutor, LibExecutor, PkgExecutor, ModuleExecutor };
|