@akanjs/cli 3.1.0-rc.4 → 3.1.0-rc.6
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-ymte1tv8.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-4rhdm1qw.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-r5x1mch5.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-3dkr7rdz.js → application.command-m1kcd7jf.js} +12 -11
- package/{applicationBuildRunner-h0j2swx1.js → applicationBuildRunner-dj0xkrep.js} +16 -9
- package/buildBatch.proc.js +7 -7
- package/{cloud.command-4kyxfw0y.js → cloud.command-3jzym8q7.js} +17 -16
- package/cloud.script-r4nskbc6.js +24 -0
- package/{code.command-sjvzt8fc.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-j9frz0dz.js → context.command-0qx447mb.js} +17 -16
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-1epw6hey.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +450 -1084
- package/guidelines/cssRule/cssRule.instruction.md +16 -1
- package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
- 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 +35 -8
- package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +76 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
- package/incrementalBuilder.proc.js +4 -4
- package/{index-jw6st2rd.js → index-192ha4xh.js} +356 -307
- package/{index-1hkr7mrb.js → index-1p98exm2.js} +2 -2
- package/{index-8byzcvxp.js → index-1xj2t7cb.js} +7 -7
- package/{index-er3hekv9.js → index-20bxxyvn.js} +80 -80
- package/{index-xe3x2kw5.js → index-35hc95h5.js} +1 -1
- package/{index-p5nrc5he.js → index-4mhy6qme.js} +4 -4
- package/{index-0qc5nd61.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/{index-z3v7abrk.js → index-665zp9rs.js} +38 -15
- package/{index-1t2xw0p0.js → index-6xn2n442.js} +4 -4
- package/{index-jg3fj8h0.js → index-86jt2a0e.js} +1 -1
- package/{index-k33me5sp.js → index-8fp3b47h.js} +419 -418
- package/{index-shastb7m.js → index-9z426msb.js} +17 -12
- package/{index-gdybjw3x.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-jppthsff.js → index-cpb3nvma.js} +57 -58
- package/{index-b60ccq2s.js → index-d47ypebv.js} +17 -15
- package/{index-gc05vqd2.js → index-d72hfmdm.js} +13 -13
- package/{index-akha197v.js → index-fmzcs8v6.js} +4 -4
- package/{index-28ftaz3d.js → index-fyq1z6kt.js} +29 -27
- package/{index-vwmv64ad.js → index-g1mbppa9.js} +238 -236
- package/{index-hedt951c.js → index-h47cf1gg.js} +7 -7
- package/{index-hfqs9jvc.js → index-j4mkhz93.js} +3 -3
- package/{index-08a5ga6n.js → index-jw1zm273.js} +28 -30
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
- package/{index-2wyn17fa.js → index-pydbnfez.js} +2 -2
- package/{index-bjh4wbtk.js → index-qwa5k1j8.js} +1 -1
- package/{index-81a1ywp6.js → index-tamvyvj3.js} +3 -3
- package/{index-dez4t5sd.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-2k0t8p3p.js → index-va06nr10.js} +1086 -1075
- package/{index-9c3cd767.js → index-vk0pa0mn.js} +20 -21
- package/{index-qyaqzh8e.js → index-w44cy4vs.js} +2216 -2216
- package/index.js +22 -22
- package/{library.command-n58dgakb.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-f2768sww.js → localRegistry.command-xxpqgxjw.js} +15 -14
- package/{module.command-xzjg5k7n.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-24sh1j1q.js → package.command-7343f74r.js} +4 -4
- package/package.json +2 -2
- package/{page.command-fqnm88na.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-vn6taz75.js → pipeline.command-a2nrk26m.js} +3 -3
- package/{primitive.command-4j1g1a0d.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-3y7ayty0.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-5hqmcdwt.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-rw5tk460.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-0jpap45m.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-y7n9sghk.js → subspace.command-92xkc0s0.js} +11 -10
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +24 -53
- package/{tunnel.command-5gjwh1f8.js → tunnel.command-2kw8vt9g.js} +8 -7
- package/{workflow.command-3t22f7eb.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-cq5xg2jq.js → workspace.command-5se6spqb.js} +28 -27
- package/TunnelShare-p8ymkww8.js +0 -13
- package/cloud.script-jc3hc5d9.js +0 -23
- package/context.runner-v0def5my.js +0 -24
- package/index-n68jx79w.js +0 -120
|
@@ -4,101 +4,284 @@ import {
|
|
|
4
4
|
FileSys2
|
|
5
5
|
} from "./index-52w0fvh8.js";
|
|
6
6
|
|
|
7
|
-
// pkgs/@akanjs/devkit/
|
|
8
|
-
import {
|
|
9
|
-
|
|
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";
|
|
10
39
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
58
|
}
|
|
22
|
-
|
|
23
|
-
|
|
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)}`;
|
|
24
68
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(`
|
|
37
99
|
`);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
proc.exited
|
|
54
|
-
]);
|
|
55
|
-
if (exitCode !== 0)
|
|
56
|
-
throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
57
|
-
}
|
|
58
|
-
static async workerEntry(workspaceRoot) {
|
|
59
|
-
const candidates = [
|
|
60
|
-
path.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
61
|
-
path.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
|
|
62
|
-
path.join(import.meta.dir, "typecheck.proc.js"),
|
|
63
|
-
path.join(import.meta.dir, "typecheck.proc.ts"),
|
|
64
|
-
path.join(import.meta.dir, "..", "typecheck.proc.js")
|
|
65
|
-
];
|
|
66
|
-
for (const candidate of candidates)
|
|
67
|
-
if (await Bun.file(candidate).exists())
|
|
68
|
-
return candidate;
|
|
69
|
-
throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
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`));
|
|
70
115
|
}
|
|
71
|
-
|
|
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
|
|
72
125
|
|
|
73
|
-
|
|
74
|
-
import
|
|
75
|
-
|
|
76
|
-
import
|
|
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}
|
|
77
133
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
static fileName(environment, profile) {
|
|
82
|
-
return `env.server.${environment}.${profile}.ts`;
|
|
83
|
-
}
|
|
84
|
-
static profileFileOf(file, profile) {
|
|
85
|
-
return file.replace(/\.ts$/, `.${profile}.ts`);
|
|
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.`;
|
|
86
137
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
179
|
+
import path from "path";
|
|
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)]));
|
|
95
194
|
}
|
|
96
|
-
static
|
|
97
|
-
return
|
|
195
|
+
static isArchive(source) {
|
|
196
|
+
return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
|
|
98
197
|
}
|
|
99
|
-
static #
|
|
100
|
-
|
|
101
|
-
|
|
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";
|
|
260
|
+
|
|
261
|
+
class ServerEnvProfile {
|
|
262
|
+
static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
|
|
263
|
+
static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
|
|
264
|
+
static fileName(environment, profile) {
|
|
265
|
+
return `env.server.${environment}.${profile}.ts`;
|
|
266
|
+
}
|
|
267
|
+
static profileFileOf(file, profile) {
|
|
268
|
+
return file.replace(/\.ts$/, `.${profile}.ts`);
|
|
269
|
+
}
|
|
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;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
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);
|
|
102
285
|
}
|
|
103
286
|
static #isSecret(value, underSecret) {
|
|
104
287
|
return underSecret && !!value.trim() || ServerEnvProfile.#isCredentialUrl(value);
|
|
@@ -327,839 +510,92 @@ class ServerEnvProfile {
|
|
|
327
510
|
}
|
|
328
511
|
}
|
|
329
512
|
|
|
330
|
-
// pkgs/@akanjs/devkit/
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return true;
|
|
383
|
-
if (ts.isCallExpression(statement.expression))
|
|
384
|
-
return true;
|
|
385
|
-
if (ts.isIdentifier(statement.expression))
|
|
386
|
-
defaultIdentifier = statement.expression.text;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
390
|
-
for (const specifier of statement.exportClause.elements) {
|
|
391
|
-
if (specifier.name.text !== "default")
|
|
392
|
-
continue;
|
|
393
|
-
defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
|
|
398
|
-
}
|
|
399
|
-
#hasModifier(node, kind) {
|
|
400
|
-
const ts = this.#ts;
|
|
401
|
-
return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
|
|
402
|
-
}
|
|
403
|
-
#isAsyncFunctionExpression(node) {
|
|
404
|
-
const ts = this.#ts;
|
|
405
|
-
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();
|
|
406
565
|
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
|
|
410
|
-
var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
|
|
411
|
-
var IMPLICIT_LAYOUT_DIR = path4.join(".akan", "generated", "root-layouts");
|
|
412
|
-
var IMPLICIT_DICT_DIR = path4.join(".akan", "generated", "dict");
|
|
413
|
-
var IMPLICIT_OVERRIDES_DIR = path4.join(".akan", "generated", "overrides");
|
|
414
|
-
var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
|
|
415
|
-
var importSpecifier = (fromAbsPath, toAbsPath) => {
|
|
416
|
-
const rel = path4.relative(path4.dirname(fromAbsPath), toAbsPath).split(path4.sep).join("/");
|
|
417
|
-
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
418
566
|
};
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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)
|
|
583
|
+
return [];
|
|
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
|
+
};
|
|
426
597
|
|
|
427
|
-
|
|
428
|
-
return createElement(UiOverrideProvider, { value }, children);
|
|
429
|
-
}
|
|
430
|
-
`;
|
|
431
|
-
await Bun.write(absPath, source);
|
|
432
|
-
return absPath;
|
|
433
|
-
}
|
|
434
|
-
async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
|
|
435
|
-
const keep = new Set(keepAbsPaths);
|
|
436
|
-
const names = await readdir(dirAbsPath).catch(() => []);
|
|
437
|
-
await Promise.all(names.map((name) => path4.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm2(absPath, { force: true })));
|
|
438
|
-
}
|
|
439
|
-
function getRootBoundarySegments(key) {
|
|
440
|
-
const match = LAYOUT_KEY_RE.exec(key);
|
|
441
|
-
if (!match)
|
|
442
|
-
return null;
|
|
443
|
-
const prefix = match[1]?.replace(/\/$/, "");
|
|
444
|
-
if (!prefix)
|
|
445
|
-
return [];
|
|
446
|
-
return prefix.split("/").filter(Boolean);
|
|
447
|
-
}
|
|
448
|
-
function implicitRootLayoutKey(segments) {
|
|
449
|
-
return `./${[...segments, "__root_layout"].join("/")}.tsx`;
|
|
450
|
-
}
|
|
451
|
-
function implicitRootLayoutAbsPath(appCwdPath, segments) {
|
|
452
|
-
const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
|
|
453
|
-
return path4.join(path4.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
|
|
454
|
-
}
|
|
455
|
-
function isRootBoundarySegments(segments, basePaths) {
|
|
456
|
-
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
457
|
-
if (firstVisibleIndex === -1)
|
|
458
|
-
return segments.length <= 1;
|
|
459
|
-
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
|
|
460
|
-
return false;
|
|
461
|
-
const visible = segments.slice(firstVisibleIndex);
|
|
462
|
-
const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
|
|
463
|
-
return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
|
|
464
|
-
}
|
|
465
|
-
function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
|
|
466
|
-
const boundaries = new Map;
|
|
467
|
-
for (const key of pageKeys) {
|
|
468
|
-
const segments = getRootBoundarySegments(key);
|
|
469
|
-
if (!segments)
|
|
470
|
-
continue;
|
|
471
|
-
if (!isRootBoundarySegments(segments, basePaths))
|
|
472
|
-
continue;
|
|
473
|
-
const id = segments.join("/");
|
|
474
|
-
boundaries.set(id, {
|
|
475
|
-
sourceKey: key,
|
|
476
|
-
sourceAbsPath: path4.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
|
|
477
|
-
segments
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
if (boundaries.size === 0)
|
|
481
|
-
boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
|
|
482
|
-
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
483
|
-
}
|
|
484
|
-
function hasAncestorRootBoundary(boundary, boundaries) {
|
|
485
|
-
return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
|
|
486
|
-
}
|
|
487
|
-
function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
|
|
488
|
-
const rootLayoutKey = pageKeys.find((key) => {
|
|
489
|
-
const segments = getRootBoundarySegments(key);
|
|
490
|
-
return segments !== null && segments.length === 0;
|
|
491
|
-
});
|
|
492
|
-
return rootLayoutKey ? path4.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
|
|
493
|
-
}
|
|
494
|
-
async function assertEnvClientConvention(appCwdPath, appName) {
|
|
495
|
-
const envPath = path4.join(appCwdPath, "env", "env.client.ts");
|
|
496
|
-
if (!await Bun.file(envPath).exists()) {
|
|
497
|
-
throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
|
|
501
|
-
const absPath = path4.join(path4.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
502
|
-
await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
|
|
503
|
-
|
|
504
|
-
export const allDictionary = getAllDictionary();
|
|
505
|
-
`);
|
|
506
|
-
return absPath;
|
|
507
|
-
}
|
|
508
|
-
async function writeGeneratedRootLayoutFile(opts) {
|
|
509
|
-
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
510
|
-
const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
|
|
511
|
-
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
512
|
-
const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
|
|
513
|
-
const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
|
|
514
|
-
const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
|
|
515
|
-
const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
|
|
516
|
-
const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
|
|
517
|
-
void st;
|
|
518
|
-
` : `import "@apps/${opts.appName}/client";
|
|
519
|
-
`;
|
|
520
|
-
const inheritedLabel = inheritedSourceAbsPath ? path4.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
|
|
521
|
-
const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
|
|
522
|
-
const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
|
|
523
|
-
` : `const inheritedLayout: LayoutModule = {};
|
|
524
|
-
`;
|
|
525
|
-
const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
|
|
526
|
-
const userLabel = opts.boundary.sourceAbsPath ? path4.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
|
|
527
|
-
const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
|
|
528
|
-
const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
|
|
529
|
-
const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
|
|
530
|
-
` : `const UserLayout = ({ children }: LayoutProps) => children;
|
|
531
|
-
const userLayout: LayoutModule = {};
|
|
532
|
-
`;
|
|
533
|
-
const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
|
|
534
|
-
const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
|
|
535
|
-
const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
|
|
536
|
-
const layoutBinding = isAsyncUserLayout ? ` const layout =
|
|
537
|
-
process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
|
|
538
|
-
? await UserLayout({ params, searchParams, children })
|
|
539
|
-
: ${userLayoutElement};
|
|
540
|
-
` : "";
|
|
541
|
-
const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
|
|
542
|
-
const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
|
|
543
|
-
const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
|
|
544
|
-
const layoutExports = `export async function generateHead(props: PageProps) {
|
|
545
|
-
const heads = await Promise.all(
|
|
546
|
-
${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
|
|
547
|
-
);
|
|
548
|
-
return RouteHead.mergeNodes(heads);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
|
|
552
|
-
export const Error = userLayout.Error ?? inheritedLayout.Error;
|
|
553
|
-
export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
|
|
554
|
-
export const Loading = userLayout.Loading;
|
|
555
|
-
|
|
556
|
-
${layoutSignature}({ children, params, searchParams }: LayoutProps) {
|
|
557
|
-
${layoutBinding} return `;
|
|
558
|
-
const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
559
|
-
import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
|
|
560
|
-
import type { ReactNode } from "react";
|
|
561
|
-
import { System } from "akanjs/ui";
|
|
562
|
-
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
563
|
-
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
564
|
-
${clientImport}${inheritedImport}${userImport}
|
|
565
|
-
const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
|
|
566
|
-
const defaultFonts = userFonts.filter((font) => font.default);
|
|
567
|
-
if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
|
|
568
|
-
const defaultFont = defaultFonts[0];
|
|
569
|
-
const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
|
|
570
|
-
export const theme = userLayout.theme ?? inheritedLayout.theme;
|
|
571
|
-
export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
|
|
572
|
-
export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
|
|
573
|
-
|
|
574
|
-
${layoutExports}(
|
|
575
|
-
<System.Provider
|
|
576
|
-
of={GeneratedLayout as never}
|
|
577
|
-
appName=${JSON.stringify(opts.appName)}
|
|
578
|
-
${prefix ? `prefix=${JSON.stringify(prefix)}
|
|
579
|
-
` : ""}params={params}
|
|
580
|
-
manifest={manifest}
|
|
581
|
-
env={env}
|
|
582
|
-
theme={theme}
|
|
583
|
-
viewport={viewport}
|
|
584
|
-
fonts={loadFonts(userFonts)}
|
|
585
|
-
className={defaultFontClassName}
|
|
586
|
-
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
587
|
-
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
588
|
-
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
589
|
-
allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
|
|
590
|
-
>
|
|
591
|
-
${layoutChild}
|
|
592
|
-
</System.Provider>
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
596
|
-
import { resolveRouteModule, RouteHead } from "akanjs/client";
|
|
597
|
-
import type { ReactNode } from "react";
|
|
598
|
-
${inheritedImport}${userImport}
|
|
599
|
-
${layoutExports}${layoutReturn};
|
|
600
|
-
}
|
|
601
|
-
`;
|
|
602
|
-
await Bun.write(absPath, source);
|
|
603
|
-
return absPath;
|
|
604
|
-
}
|
|
605
|
-
async function resolveSsrPageEntries(opts) {
|
|
606
|
-
const absPageDir = path4.resolve(opts.appCwdPath, "page");
|
|
607
|
-
const hasSt = await Bun.file(path4.join(opts.appCwdPath, "lib", "st.ts")).exists();
|
|
608
|
-
const basePaths = opts.basePaths ?? [];
|
|
609
|
-
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
610
|
-
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
611
|
-
const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
|
|
612
|
-
const segments = getRootBoundarySegments(key);
|
|
613
|
-
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
614
|
-
}));
|
|
615
|
-
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
616
|
-
const userAbsPath = path4.resolve(absPageDir, key);
|
|
617
|
-
if (OVERRIDES_KEY_RE.test(key)) {
|
|
618
|
-
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
619
|
-
appCwdPath: opts.appCwdPath,
|
|
620
|
-
key,
|
|
621
|
-
userAbsPath
|
|
622
|
-
});
|
|
623
|
-
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
624
|
-
}
|
|
625
|
-
return { key, moduleAbsPath: userAbsPath };
|
|
626
|
-
}));
|
|
627
|
-
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
628
|
-
key: implicitRootLayoutKey(boundary.segments),
|
|
629
|
-
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
630
|
-
appCwdPath: opts.appCwdPath,
|
|
631
|
-
appName: opts.appName,
|
|
632
|
-
boundary,
|
|
633
|
-
rootSourceAbsPath,
|
|
634
|
-
includeStInit: hasSt && boundary.segments.length === 0,
|
|
635
|
-
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
|
|
636
|
-
}),
|
|
637
|
-
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
|
|
638
|
-
})));
|
|
639
|
-
const appCwdAbsPath = path4.resolve(opts.appCwdPath);
|
|
640
|
-
await Promise.all([
|
|
641
|
-
pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
|
|
642
|
-
pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
|
|
643
|
-
]);
|
|
644
|
-
return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
|
|
645
|
-
}
|
|
646
|
-
async function resolveSsrPageEntriesForApp(app, pageKeys) {
|
|
647
|
-
const config = await app.getConfig();
|
|
648
|
-
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// pkgs/@akanjs/devkit/codegenLock.ts
|
|
652
|
-
import { mkdir as mkdir2, open, readFile, rm as rm3, stat } from "fs/promises";
|
|
653
|
-
import path5 from "path";
|
|
654
|
-
import { Logger as Logger2 } from "akanjs/common";
|
|
655
|
-
|
|
656
|
-
class CodegenLock {
|
|
657
|
-
static fileName = "codegen.lock";
|
|
658
|
-
static waitTimeoutMs = 1e4;
|
|
659
|
-
static unknownHolderStaleMs = 60000;
|
|
660
|
-
static #pollMs = 25;
|
|
661
|
-
static #logger = new Logger2("CodegenLock");
|
|
662
|
-
static #queue = Promise.resolve();
|
|
663
|
-
static pathIn(workspaceRoot) {
|
|
664
|
-
return path5.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
|
|
665
|
-
}
|
|
666
|
-
static async run(workspaceRoot, label, fn) {
|
|
667
|
-
const ahead = CodegenLock.#queue;
|
|
668
|
-
let done;
|
|
669
|
-
CodegenLock.#queue = new Promise((resolve) => {
|
|
670
|
-
done = resolve;
|
|
671
|
-
});
|
|
672
|
-
try {
|
|
673
|
-
await CodegenLock.#waitForQueue(ahead, label);
|
|
674
|
-
return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
|
|
675
|
-
} finally {
|
|
676
|
-
done();
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
static async#waitForQueue(ahead, label) {
|
|
680
|
-
let timer = null;
|
|
681
|
-
const expired = new Promise((resolve) => {
|
|
682
|
-
timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
|
|
683
|
-
});
|
|
684
|
-
try {
|
|
685
|
-
if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
|
|
686
|
-
CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
|
|
687
|
-
} finally {
|
|
688
|
-
if (timer)
|
|
689
|
-
clearTimeout(timer);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
static async#withFileLock(workspaceRoot, label, fn) {
|
|
693
|
-
const lockPath = CodegenLock.pathIn(workspaceRoot);
|
|
694
|
-
await mkdir2(path5.dirname(lockPath), { recursive: true }).catch(() => {
|
|
695
|
-
return;
|
|
696
|
-
});
|
|
697
|
-
const held = await CodegenLock.#acquire(lockPath, label);
|
|
698
|
-
try {
|
|
699
|
-
return await fn();
|
|
700
|
-
} finally {
|
|
701
|
-
if (held)
|
|
702
|
-
await rm3(lockPath, { force: true }).catch(() => {
|
|
703
|
-
return;
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
static async#acquire(lockPath, label) {
|
|
708
|
-
const deadline = Date.now() + CodegenLock.waitTimeoutMs;
|
|
709
|
-
for (;; ) {
|
|
710
|
-
const handle = await open(lockPath, "wx").catch(() => null);
|
|
711
|
-
if (handle) {
|
|
712
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
|
|
713
|
-
return;
|
|
714
|
-
});
|
|
715
|
-
await handle.close().catch(() => {
|
|
716
|
-
return;
|
|
717
|
-
});
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
if (await CodegenLock.#reclaimIfAbandoned(lockPath))
|
|
721
|
-
continue;
|
|
722
|
-
if (Date.now() >= deadline) {
|
|
723
|
-
CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
await Bun.sleep(CodegenLock.#pollMs);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
static async#reclaimIfAbandoned(lockPath) {
|
|
730
|
-
const info = await stat(lockPath).catch(() => null);
|
|
731
|
-
if (!info)
|
|
732
|
-
return true;
|
|
733
|
-
const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
|
|
734
|
-
if (holder) {
|
|
735
|
-
if (CodegenLock.#isAlive(holder.pid))
|
|
736
|
-
return false;
|
|
737
|
-
} else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
|
|
738
|
-
return false;
|
|
739
|
-
await rm3(lockPath, { force: true }).catch(() => {
|
|
740
|
-
return;
|
|
741
|
-
});
|
|
742
|
-
return true;
|
|
743
|
-
}
|
|
744
|
-
static #parseHolder(raw) {
|
|
745
|
-
try {
|
|
746
|
-
const parsed = JSON.parse(raw);
|
|
747
|
-
if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
|
|
748
|
-
return null;
|
|
749
|
-
return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
|
|
750
|
-
} catch {
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
static #isAlive(pid) {
|
|
755
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
756
|
-
return false;
|
|
757
|
-
try {
|
|
758
|
-
process.kill(pid, 0);
|
|
759
|
-
return true;
|
|
760
|
-
} catch (error) {
|
|
761
|
-
return error.code === "EPERM";
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// pkgs/@akanjs/devkit/spinner.ts
|
|
767
|
-
import ora from "ora";
|
|
768
|
-
|
|
769
|
-
class Spinner {
|
|
770
|
-
static padding = 12;
|
|
771
|
-
static oraOptions = { discardStdin: false };
|
|
772
|
-
static canAnimate(stream = process.stderr) {
|
|
773
|
-
return !stream.isTTY || stream.columns > 0;
|
|
774
|
-
}
|
|
775
|
-
spinner;
|
|
776
|
-
stopWatch = null;
|
|
777
|
-
startAt = new Date;
|
|
778
|
-
prefix;
|
|
779
|
-
message;
|
|
780
|
-
enableSpin;
|
|
781
|
-
constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
|
|
782
|
-
Spinner.padding = Math.max(Spinner.padding, prefix.length);
|
|
783
|
-
this.prefix = prefix;
|
|
784
|
-
this.message = message;
|
|
785
|
-
this.spinner = ora({ ...Spinner.oraOptions, text: message });
|
|
786
|
-
this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
|
|
787
|
-
this.spinner.indent = indent;
|
|
788
|
-
this.enableSpin = enableSpin && Spinner.canAnimate();
|
|
789
|
-
}
|
|
790
|
-
start() {
|
|
791
|
-
this.startAt = new Date;
|
|
792
|
-
if (this.enableSpin) {
|
|
793
|
-
this.spinner.start();
|
|
794
|
-
this.stopWatch = setInterval(() => {
|
|
795
|
-
this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
|
|
796
|
-
this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
|
|
797
|
-
}, 1000);
|
|
798
|
-
} else
|
|
799
|
-
this.spinner.info();
|
|
800
|
-
return this;
|
|
801
|
-
}
|
|
802
|
-
succeed(message) {
|
|
803
|
-
this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
|
|
804
|
-
this.#reset();
|
|
805
|
-
}
|
|
806
|
-
fail(message) {
|
|
807
|
-
this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
|
|
808
|
-
this.#reset();
|
|
809
|
-
}
|
|
810
|
-
isSpinning() {
|
|
811
|
-
return this.spinner.isSpinning;
|
|
812
|
-
}
|
|
813
|
-
#reset() {
|
|
814
|
-
if (this.stopWatch)
|
|
815
|
-
clearInterval(this.stopWatch);
|
|
816
|
-
this.stopWatch = null;
|
|
817
|
-
}
|
|
818
|
-
#getElapsedTimeStr({ floor = false } = {}) {
|
|
819
|
-
const ms = Date.now() - this.startAt.getTime();
|
|
820
|
-
if (ms < 1000)
|
|
821
|
-
return `${ms}ms`;
|
|
822
|
-
const s = Math.floor(ms / 1000);
|
|
823
|
-
if (s < 60)
|
|
824
|
-
return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
|
|
825
|
-
const m = Math.floor(s / 60);
|
|
826
|
-
return `${m}m ${s % 60}s`;
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// pkgs/@akanjs/devkit/executors.ts
|
|
831
|
-
import {
|
|
832
|
-
exec,
|
|
833
|
-
fork,
|
|
834
|
-
spawn as spawn2
|
|
835
|
-
} from "child_process";
|
|
836
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
837
|
-
import {
|
|
838
|
-
copyFile,
|
|
839
|
-
cp as cpEntry,
|
|
840
|
-
lstat,
|
|
841
|
-
mkdir as mkdir3,
|
|
842
|
-
readdir as readDirEntries,
|
|
843
|
-
realpath,
|
|
844
|
-
rm as rm4,
|
|
845
|
-
stat as stat2,
|
|
846
|
-
symlink
|
|
847
|
-
} from "fs/promises";
|
|
848
|
-
import path13 from "path";
|
|
849
|
-
import { pathToFileURL } from "url";
|
|
850
|
-
import {
|
|
851
|
-
capitalize,
|
|
852
|
-
getPageSourceFileViolation,
|
|
853
|
-
isRouteSourceFile,
|
|
854
|
-
Logger as Logger3,
|
|
855
|
-
parseRouteModuleKey,
|
|
856
|
-
toError as toError2,
|
|
857
|
-
validatePageSourceFile,
|
|
858
|
-
validateSubRoutePageKey
|
|
859
|
-
} from "akanjs/common";
|
|
860
|
-
var {$ } = globalThis.Bun;
|
|
861
|
-
import chalk2 from "chalk";
|
|
862
|
-
|
|
863
|
-
// pkgs/@akanjs/devkit/agentsIndex.ts
|
|
864
|
-
var recipeScannerLoad = null;
|
|
865
|
-
var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
|
|
866
|
-
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
867
|
-
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
868
|
-
var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
869
|
-
var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
|
|
870
|
-
|
|
871
|
-
${block}`;
|
|
872
|
-
var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
873
|
-
var readDevkitVersion = async () => {
|
|
874
|
-
const { readFile } = await import("fs/promises");
|
|
875
|
-
const { getDirname } = await import("./fileSys-wzzrntkw.js");
|
|
876
|
-
try {
|
|
877
|
-
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
878
|
-
return JSON.parse(raw).version ?? null;
|
|
879
|
-
} catch {
|
|
880
|
-
return null;
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
var upsertAgentBlock = (existing, block) => {
|
|
884
|
-
const managed = `${AGENT_BLOCK_START}
|
|
885
|
-
${block}
|
|
886
|
-
${AGENT_BLOCK_END}`;
|
|
887
|
-
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
888
|
-
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
889
|
-
if (startIndex >= 0 && endIndex > startIndex) {
|
|
890
|
-
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
891
|
-
}
|
|
892
|
-
return `${existing.replace(/\s*$/, "")}
|
|
893
|
-
|
|
894
|
-
${managed}
|
|
895
|
-
`;
|
|
896
|
-
};
|
|
897
|
-
var extractAgentBlock = (content) => {
|
|
898
|
-
const startIndex = content.indexOf(AGENT_BLOCK_START);
|
|
899
|
-
const endIndex = content.indexOf(AGENT_BLOCK_END);
|
|
900
|
-
if (startIndex < 0 || endIndex <= startIndex)
|
|
901
|
-
return null;
|
|
902
|
-
return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
|
|
903
|
-
};
|
|
904
|
-
var signatureOf = (recipe) => {
|
|
905
|
-
const entries = Object.entries(recipe.variants);
|
|
906
|
-
if (entries.length === 0)
|
|
907
|
-
return "";
|
|
908
|
-
const parts = entries.map(([key, values]) => {
|
|
909
|
-
if (values.length === 1 && values[0] === "true")
|
|
910
|
-
return `${key}?`;
|
|
911
|
-
const def = recipe.defaultVariants?.[key];
|
|
912
|
-
return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
|
|
913
|
-
});
|
|
914
|
-
return `(${parts.join(" \xB7 ")})`;
|
|
915
|
-
};
|
|
916
|
-
var renderRecipeEntries = (recipes) => {
|
|
917
|
-
const groups = new Map;
|
|
918
|
-
for (const recipe of recipes)
|
|
919
|
-
groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
|
|
920
|
-
return [...groups.entries()].map(([importFrom, list]) => {
|
|
921
|
-
const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
|
|
922
|
-
`);
|
|
923
|
-
return `Import from \`${importFrom}\`:
|
|
924
|
-
${items}`;
|
|
925
|
-
}).join(`
|
|
926
|
-
|
|
927
|
-
`);
|
|
928
|
-
};
|
|
929
|
-
var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
|
|
930
|
-
const { collectRecipeSources } = await loadRecipeScanner();
|
|
931
|
-
const sources = [
|
|
932
|
-
...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
|
|
933
|
-
];
|
|
934
|
-
for (const lib of [...new Set(libDeps)].sort()) {
|
|
935
|
-
if (scope.type === "lib" && lib === scope.name)
|
|
936
|
-
continue;
|
|
937
|
-
sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
|
|
938
|
-
}
|
|
939
|
-
return sources;
|
|
940
|
-
};
|
|
941
|
-
var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
|
|
942
|
-
const { scanRecipes } = await loadRecipeScanner();
|
|
943
|
-
return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
|
|
944
|
-
};
|
|
945
|
-
var renderScopeAgentBlock = (scope, recipes) => {
|
|
946
|
-
const scopePath = `${scope.type}s/${scope.name}`;
|
|
947
|
-
const intro = `## Recipes In Scope
|
|
948
|
-
|
|
949
|
-
UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
|
|
950
|
-
\`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
|
|
951
|
-
arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
|
|
952
|
-
boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
|
|
953
|
-
and verified by \`akan lint ${scope.name}\`.`;
|
|
954
|
-
if (recipes.length === 0) {
|
|
955
|
-
return `${intro}
|
|
956
|
-
|
|
957
|
-
No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
|
|
958
|
-
root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
|
|
959
|
-
folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
|
|
960
|
-
}
|
|
961
|
-
return `${intro}
|
|
962
|
-
|
|
963
|
-
${renderRecipeEntries(recipes)}`;
|
|
964
|
-
};
|
|
965
|
-
var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
|
|
966
|
-
|
|
967
|
-
Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
|
|
968
|
-
root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
|
|
969
|
-
section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
|
|
970
|
-
markers freely.
|
|
971
|
-
|
|
972
|
-
${AGENT_BLOCK_START}
|
|
973
|
-
${block}
|
|
974
|
-
${AGENT_BLOCK_END}
|
|
975
|
-
`;
|
|
976
|
-
var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
|
|
977
|
-
|
|
978
|
-
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
979
|
-
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
980
|
-
|
|
981
|
-
Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
|
|
982
|
-
|
|
983
|
-
- Restates the identifier, the signature, or the line under it \u2192 delete it.
|
|
984
|
-
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
|
|
985
|
-
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
|
|
986
|
-
- Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
|
|
987
|
-
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
988
|
-
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
|
|
989
|
-
keep it, one line.
|
|
990
|
-
|
|
991
|
-
That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
992
|
-
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
993
|
-
surrounding code carries none, your diff carries none.`;
|
|
994
|
-
var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
|
|
995
|
-
|
|
996
|
-
@AGENTS.md
|
|
997
|
-
|
|
998
|
-
${CLAUDE_COMMENT_RULE}
|
|
999
|
-
`;
|
|
1000
|
-
|
|
1001
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
1002
|
-
import path6 from "path";
|
|
1003
|
-
|
|
1004
|
-
// pkgs/@akanjs/devkit/akanConfig/types.ts
|
|
1005
|
-
import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
|
|
1006
|
-
|
|
1007
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
1008
|
-
class AkanBin {
|
|
1009
|
-
static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
|
|
1010
|
-
static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1011
|
-
static parse(declared, owner, baseDir) {
|
|
1012
|
-
if (declared === undefined)
|
|
1013
|
-
return {};
|
|
1014
|
-
if (!declared || typeof declared !== "object" || Array.isArray(declared))
|
|
1015
|
-
throw new Error(`${owner}: bin maps each executable's name to its platforms`);
|
|
1016
|
-
return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
|
|
1017
|
-
}
|
|
1018
|
-
static isArchive(source) {
|
|
1019
|
-
return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
|
|
1020
|
-
}
|
|
1021
|
-
static #platforms(name, platforms, owner, baseDir) {
|
|
1022
|
-
if (!AkanBin.fileName.test(name))
|
|
1023
|
-
throw new Error(`${owner}: bin.${name} is not a file name`);
|
|
1024
|
-
if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
|
|
1025
|
-
throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
|
|
1026
|
-
const resolved = {};
|
|
1027
|
-
for (const [platform, source] of Object.entries(platforms)) {
|
|
1028
|
-
if (!binPlatforms.includes(platform))
|
|
1029
|
-
throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
|
|
1030
|
-
resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
|
|
1031
|
-
}
|
|
1032
|
-
return resolved;
|
|
1033
|
-
}
|
|
1034
|
-
static #source(at, source, baseDir) {
|
|
1035
|
-
if (!source || typeof source !== "object" || Array.isArray(source))
|
|
1036
|
-
throw new Error(`${at} must be { url, sha256 } or { path }`);
|
|
1037
|
-
const { url, sha256, path: localPath, file, ...rest } = source;
|
|
1038
|
-
const extra = Object.keys(rest);
|
|
1039
|
-
if (extra.length)
|
|
1040
|
-
throw new Error(`${at}: unknown key ${extra.join(", ")}`);
|
|
1041
|
-
if (url === undefined === (localPath === undefined))
|
|
1042
|
-
throw new Error(`${at} takes either url and sha256, or path`);
|
|
1043
|
-
const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
|
|
1044
|
-
const archive = AkanBin.isArchive(resolved);
|
|
1045
|
-
if (file === undefined) {
|
|
1046
|
-
if (archive)
|
|
1047
|
-
throw new Error(`${at} names an archive: file picks the executable inside it`);
|
|
1048
|
-
return resolved;
|
|
1049
|
-
}
|
|
1050
|
-
if (!archive)
|
|
1051
|
-
throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
|
|
1052
|
-
const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
|
|
1053
|
-
if (!parts.length || path6.isAbsolute(file) || parts.includes(".."))
|
|
1054
|
-
throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
|
|
1055
|
-
return { ...resolved, file: parts.join("/") };
|
|
1056
|
-
}
|
|
1057
|
-
static #urlSource(at, url, sha256) {
|
|
1058
|
-
if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
|
|
1059
|
-
throw new Error(`${at}: url must be an http(s) address`);
|
|
1060
|
-
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
|
|
1061
|
-
throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
|
|
1062
|
-
return { url, sha256: sha256.toLowerCase() };
|
|
1063
|
-
}
|
|
1064
|
-
static #pathSource(at, localPath, sha256, baseDir) {
|
|
1065
|
-
if (typeof localPath !== "string" || !localPath)
|
|
1066
|
-
throw new Error(`${at}: path must name a file`);
|
|
1067
|
-
if (sha256 !== undefined)
|
|
1068
|
-
throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
|
|
1069
|
-
return { path: path6.resolve(baseDir, localPath) };
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
// pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
|
|
1073
|
-
import fs2 from "fs";
|
|
1074
|
-
import path7 from "path";
|
|
1075
|
-
import { fileURLToPath } from "url";
|
|
1076
|
-
import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
|
|
1077
|
-
import { resolveAkanI18nConfig } from "akanjs/common";
|
|
1078
|
-
var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
1079
|
-
var DEFAULT_OPTIMIZE_IMPORTS = [
|
|
1080
|
-
"lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
|
|
1081
|
-
"@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
|
|
1082
|
-
"@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
|
|
1083
|
-
"@material-ui/icons @tabler/icons-react mui-core react-icons/*"
|
|
1084
|
-
].flatMap((line) => line.split(" "));
|
|
1085
|
-
var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
|
|
1086
|
-
var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
1087
|
-
var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
|
|
1088
|
-
var DEFAULT_AKAN_IMAGE_CONFIG = {
|
|
1089
|
-
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
1090
|
-
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
1091
|
-
formats: ["image/webp"],
|
|
1092
|
-
qualities: [75],
|
|
1093
|
-
minimumCacheTTL: 14400,
|
|
1094
|
-
remotePatterns: [],
|
|
1095
|
-
localPatterns: [{ pathname: "/**" }],
|
|
1096
|
-
dangerouslyAllowSVG: false,
|
|
1097
|
-
maximumRedirects: 3,
|
|
1098
|
-
fetchTimeoutMs: 7000,
|
|
1099
|
-
maxRemoteBytes: 25 * 1024 * 1024,
|
|
1100
|
-
maxConcurrency: 0
|
|
1101
|
-
};
|
|
1102
|
-
var normalizeIndexPath = (indexPath) => {
|
|
1103
|
-
const normalized = indexPath?.trim();
|
|
1104
|
-
if (!normalized)
|
|
1105
|
-
return;
|
|
1106
|
-
const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
1107
|
-
return path === "/" ? "/" : path;
|
|
1108
|
-
};
|
|
1109
|
-
var normalizeStringList = (values) => {
|
|
1110
|
-
const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
1111
|
-
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
|
1112
|
-
};
|
|
1113
|
-
var sanitizeAppIdSegment = (value) => {
|
|
1114
|
-
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1115
|
-
if (!cleaned)
|
|
1116
|
-
return "app";
|
|
1117
|
-
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
|
|
1118
|
-
};
|
|
1119
|
-
var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
|
|
1120
|
-
var normalizeDeepLinkDomain = (domain) => {
|
|
1121
|
-
const normalized = domain.trim();
|
|
1122
|
-
if (!normalized)
|
|
1123
|
-
return "";
|
|
1124
|
-
try {
|
|
1125
|
-
const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
|
|
1126
|
-
return url.host.toLowerCase();
|
|
1127
|
-
} catch {
|
|
1128
|
-
return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
var normalizeDeepLinks = (deepLinks) => {
|
|
1132
|
-
const schemes = normalizeStringList(deepLinks?.schemes);
|
|
1133
|
-
const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
|
|
1134
|
-
if (!schemes && !domains)
|
|
1135
|
-
return;
|
|
1136
|
-
return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
|
|
1137
|
-
};
|
|
1138
|
-
var emptyLibContributions = () => ({
|
|
1139
|
-
externalLibs: [],
|
|
1140
|
-
trustedDependencies: [],
|
|
1141
|
-
bin: [],
|
|
1142
|
-
docker: { preRuns: [], postRuns: [] },
|
|
1143
|
-
keepFonts: []
|
|
1144
|
-
});
|
|
1145
|
-
var normalizePackageNames = (names, owner) => {
|
|
1146
|
-
if (names === undefined)
|
|
1147
|
-
return [];
|
|
1148
|
-
if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
|
|
1149
|
-
throw new Error(`${owner}: trustedDependencies lists package names`);
|
|
1150
|
-
return [...new Set(names.map((name) => name.trim()))];
|
|
1151
|
-
};
|
|
1152
|
-
var normalizeKeepFonts = (keepFonts) => [
|
|
1153
|
-
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
|
|
1154
|
-
];
|
|
1155
|
-
var dedupeDockerRuns = (runs) => {
|
|
1156
|
-
const byKey = new Map;
|
|
1157
|
-
for (const run of runs)
|
|
1158
|
-
byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
1159
|
-
return [...byKey.values()];
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
class LibConfigRefusal extends Error {
|
|
598
|
+
class LibConfigRefusal extends Error {
|
|
1163
599
|
}
|
|
1164
600
|
|
|
1165
601
|
class AkanAppConfig {
|
|
@@ -1496,7 +932,7 @@ class AkanAppConfig {
|
|
|
1496
932
|
}
|
|
1497
933
|
#applyRoutes(routes = []) {
|
|
1498
934
|
for (const route of routes) {
|
|
1499
|
-
const basePath = route.basePath ? route.basePath
|
|
935
|
+
const basePath = route.basePath ? trimSlashes(route.basePath) : null;
|
|
1500
936
|
if (basePath !== null)
|
|
1501
937
|
this.basePaths.add(basePath);
|
|
1502
938
|
const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
|
|
@@ -1598,7 +1034,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
1598
1034
|
static async from(app, { bustImportCache = false } = {}) {
|
|
1599
1035
|
const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
|
|
1600
1036
|
AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
|
|
1601
|
-
WorkspaceExecutor.getBaseDevEnv(
|
|
1037
|
+
WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
|
|
1602
1038
|
app.workspace.getLibs(),
|
|
1603
1039
|
app.workspace.getPackageJson()
|
|
1604
1040
|
]);
|
|
@@ -1694,143 +1130,579 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
1694
1130
|
};
|
|
1695
1131
|
}
|
|
1696
1132
|
}
|
|
1697
|
-
var akanPackageJson = null;
|
|
1698
|
-
function getAkanPackageJson() {
|
|
1699
|
-
if (akanPackageJson)
|
|
1700
|
-
return akanPackageJson;
|
|
1701
|
-
const sourceDir =
|
|
1702
|
-
const packageJsonPaths = [
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
];
|
|
1707
|
-
try {
|
|
1708
|
-
packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
|
|
1709
|
-
} catch {}
|
|
1710
|
-
for (const packageJsonPath of packageJsonPaths) {
|
|
1711
|
-
try {
|
|
1712
|
-
akanPackageJson = JSON.parse(
|
|
1713
|
-
return akanPackageJson;
|
|
1714
|
-
} catch {}
|
|
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.`);
|
|
1197
|
+
if ("subspace" in config)
|
|
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 ?? []);
|
|
1200
|
+
}
|
|
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)
|
|
1242
|
+
return null;
|
|
1243
|
+
const refuse = (why) => {
|
|
1244
|
+
throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
|
|
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(/\/+$/, "")}`;
|
|
1260
|
+
}
|
|
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;
|
|
1275
|
+
}
|
|
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
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
#ts;
|
|
1287
|
+
constructor(typescript) {
|
|
1288
|
+
this.#ts = typescript;
|
|
1289
|
+
}
|
|
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;
|
|
1330
|
+
}
|
|
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
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
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("/")));
|
|
1415
|
+
}
|
|
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`);
|
|
1715
1430
|
}
|
|
1716
|
-
akanPackageJson = {
|
|
1717
|
-
name: "akanjs",
|
|
1718
|
-
version: "0.0.0",
|
|
1719
|
-
description: "akanjs",
|
|
1720
|
-
dependencies: {}
|
|
1721
|
-
};
|
|
1722
|
-
return akanPackageJson;
|
|
1723
1431
|
}
|
|
1724
|
-
function
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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);
|
|
1735
1481
|
}
|
|
1736
1482
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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);
|
|
1597
|
+
}
|
|
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
|
+
}
|
|
1610
|
+
}
|
|
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
|
+
}
|
|
1754
1623
|
}
|
|
1755
|
-
static async
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
+
}
|
|
1764
1638
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
};
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
return new AkanWorkspaceConfig(file, input);
|
|
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
|
+
}
|
|
1787
1660
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
|
|
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;
|
|
1803
1675
|
}
|
|
1804
|
-
static #
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
return refuse("that is not an object");
|
|
1812
|
-
const unknown = Object.keys(cloud).filter((key) => key !== "host");
|
|
1813
|
-
if (unknown.length)
|
|
1814
|
-
return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
|
|
1815
|
-
const { host } = cloud;
|
|
1816
|
-
if (host === undefined)
|
|
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 {
|
|
1817
1683
|
return null;
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
if (
|
|
1822
|
-
return
|
|
1823
|
-
|
|
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
|
+
}
|
|
1824
1695
|
}
|
|
1825
1696
|
}
|
|
1697
|
+
|
|
1826
1698
|
// pkgs/@akanjs/devkit/jsonFormatter.ts
|
|
1827
|
-
import
|
|
1699
|
+
import path8 from "path";
|
|
1828
1700
|
|
|
1829
1701
|
class JsonFormatter {
|
|
1830
1702
|
static lineWidth = 120;
|
|
1831
1703
|
static stringify(value, filePath) {
|
|
1832
1704
|
const json = JSON.stringify(value, null, 2);
|
|
1833
|
-
if (
|
|
1705
|
+
if (path8.basename(filePath) === "package.json")
|
|
1834
1706
|
return `${json}
|
|
1835
1707
|
`;
|
|
1836
1708
|
return `${JsonFormatter.#print(JSON.parse(json), 0, { column: 0, trailing: 0 })}
|
|
@@ -1901,7 +1773,7 @@ ${" ".repeat(depth)}]`;
|
|
|
1901
1773
|
// pkgs/@akanjs/devkit/linter.ts
|
|
1902
1774
|
import { spawn } from "child_process";
|
|
1903
1775
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1904
|
-
import
|
|
1776
|
+
import path9 from "path";
|
|
1905
1777
|
import chalk from "chalk";
|
|
1906
1778
|
var parseBiomeReport = (output) => {
|
|
1907
1779
|
for (let start = output.indexOf("{");start !== -1; start = output.indexOf("{", start + 1)) {
|
|
@@ -1967,13 +1839,13 @@ class Linter {
|
|
|
1967
1839
|
#biomeBin;
|
|
1968
1840
|
constructor(cwdPath) {
|
|
1969
1841
|
this.lintRoot = this.#findBiomeRootPath(cwdPath);
|
|
1970
|
-
this.configPath = Linter.#configPathIn(this.lintRoot) ??
|
|
1971
|
-
const localBiomeBin =
|
|
1842
|
+
this.configPath = Linter.#configPathIn(this.lintRoot) ?? path9.join(this.lintRoot, "biome.json");
|
|
1843
|
+
const localBiomeBin = path9.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
|
|
1972
1844
|
this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
|
|
1973
1845
|
}
|
|
1974
1846
|
static #configPathIn(dir) {
|
|
1975
1847
|
for (const fileName of BIOME_CONFIG_FILES) {
|
|
1976
|
-
const configPath =
|
|
1848
|
+
const configPath = path9.join(dir, fileName);
|
|
1977
1849
|
if (existsSync2(configPath))
|
|
1978
1850
|
return configPath;
|
|
1979
1851
|
}
|
|
@@ -1982,19 +1854,19 @@ class Linter {
|
|
|
1982
1854
|
#findBiomeRootPath(dir) {
|
|
1983
1855
|
if (Linter.#configPathIn(dir))
|
|
1984
1856
|
return dir;
|
|
1985
|
-
const parentDir =
|
|
1857
|
+
const parentDir = path9.dirname(dir);
|
|
1986
1858
|
if (parentDir === dir)
|
|
1987
1859
|
throw new Error(`biome.json not found from ${dir}`);
|
|
1988
1860
|
return this.#findBiomeRootPath(parentDir);
|
|
1989
1861
|
}
|
|
1990
1862
|
#toBiomePath(filePath) {
|
|
1991
|
-
const relativePath =
|
|
1992
|
-
if (!relativePath.startsWith("..") && !
|
|
1863
|
+
const relativePath = path9.relative(this.lintRoot, filePath);
|
|
1864
|
+
if (!relativePath.startsWith("..") && !path9.isAbsolute(relativePath))
|
|
1993
1865
|
return relativePath;
|
|
1994
1866
|
return filePath;
|
|
1995
1867
|
}
|
|
1996
1868
|
#resolveFilePath(filePath) {
|
|
1997
|
-
return
|
|
1869
|
+
return path9.isAbsolute(filePath) ? filePath : path9.join(this.lintRoot, filePath);
|
|
1998
1870
|
}
|
|
1999
1871
|
async#runBiome(args, input) {
|
|
2000
1872
|
return await new Promise((resolve, reject) => {
|
|
@@ -2019,7 +1891,7 @@ class Linter {
|
|
|
2019
1891
|
const diagnosticPath = diagnostic.location?.path;
|
|
2020
1892
|
if (!diagnosticPath)
|
|
2021
1893
|
return fallbackFilePath;
|
|
2022
|
-
return
|
|
1894
|
+
return path9.isAbsolute(diagnosticPath) ? diagnosticPath : path9.join(this.lintRoot, diagnosticPath);
|
|
2023
1895
|
}
|
|
2024
1896
|
#createLintMessage(diagnostic) {
|
|
2025
1897
|
const start = diagnostic.location?.start;
|
|
@@ -2355,7 +2227,7 @@ class ManifestDependencies {
|
|
|
2355
2227
|
|
|
2356
2228
|
// pkgs/@akanjs/devkit/repoIdentity.ts
|
|
2357
2229
|
import { execFileSync } from "child_process";
|
|
2358
|
-
import
|
|
2230
|
+
import path10 from "path";
|
|
2359
2231
|
var resolved = new Map;
|
|
2360
2232
|
var readRemoteName = (workspaceRoot) => {
|
|
2361
2233
|
try {
|
|
@@ -2373,13 +2245,13 @@ var resolveRepoName = (workspaceRoot) => {
|
|
|
2373
2245
|
const cached = resolved.get(workspaceRoot);
|
|
2374
2246
|
if (cached)
|
|
2375
2247
|
return cached;
|
|
2376
|
-
const repoName = readRemoteName(workspaceRoot) ??
|
|
2248
|
+
const repoName = readRemoteName(workspaceRoot) ?? path10.basename(workspaceRoot);
|
|
2377
2249
|
resolved.set(workspaceRoot, repoName);
|
|
2378
2250
|
return repoName;
|
|
2379
2251
|
};
|
|
2380
2252
|
|
|
2381
2253
|
// pkgs/@akanjs/devkit/scanInfo.ts
|
|
2382
|
-
import
|
|
2254
|
+
import path11 from "path";
|
|
2383
2255
|
|
|
2384
2256
|
// pkgs/@akanjs/devkit/workspaceLayout.ts
|
|
2385
2257
|
var appRootAllowedFiles = new Set([
|
|
@@ -2500,10 +2372,10 @@ var moduleUiFileTypes = {
|
|
|
2500
2372
|
service: new Set(["Util", "Zone"]),
|
|
2501
2373
|
scalar: new Set(["Template", "Unit"])
|
|
2502
2374
|
};
|
|
2503
|
-
var createDependencyScanner = async (exec) => (await import("./dependencyScanner-
|
|
2504
|
-
var getScanPath = (exec, relativePath) =>
|
|
2375
|
+
var createDependencyScanner = async (exec) => (await import("./dependencyScanner-3afj2vt1.js")).TypeScriptDependencyScanner.from(exec);
|
|
2376
|
+
var getScanPath = (exec, relativePath) => path11.posix.join(`${exec.type}s`, exec.name, relativePath.split(path11.sep).join("/"));
|
|
2505
2377
|
var getModuleNameFromPath = (kind, modulePath) => {
|
|
2506
|
-
const dirname =
|
|
2378
|
+
const dirname = path11.basename(modulePath);
|
|
2507
2379
|
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
2508
2380
|
};
|
|
2509
2381
|
async function assertScanConvention(exec, libRoot) {
|
|
@@ -2524,28 +2396,28 @@ async function assertScanConvention(exec, libRoot) {
|
|
|
2524
2396
|
for (const filename of await exec.readdir(facet)) {
|
|
2525
2397
|
const shadow = facetIndexShadowOf(`${facet}/${filename}`);
|
|
2526
2398
|
if (shadow)
|
|
2527
|
-
addViolation(
|
|
2399
|
+
addViolation(path11.join(facet, filename), shadow);
|
|
2528
2400
|
}
|
|
2529
2401
|
if (exec.type === "lib")
|
|
2530
2402
|
for (const { relativePath, reason } of await exec.getPageConventionViolations())
|
|
2531
2403
|
addViolation(relativePath, reason);
|
|
2532
2404
|
for (const filename of libRoot.files)
|
|
2533
2405
|
if (!isAllowedLibFacetRootFile(filename))
|
|
2534
|
-
addViolation(
|
|
2406
|
+
addViolation(path11.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
|
|
2535
2407
|
for (const dirname of libRoot.dirs)
|
|
2536
2408
|
if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
|
|
2537
|
-
addViolation(
|
|
2409
|
+
addViolation(path11.join("lib", dirname), "unsupported internal lib folder");
|
|
2538
2410
|
const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
|
|
2539
2411
|
const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
|
|
2540
2412
|
const scalarDirs = await exec.readdir("lib/__scalar");
|
|
2541
2413
|
await Promise.all([
|
|
2542
|
-
...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database",
|
|
2543
|
-
...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service",
|
|
2544
|
-
...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar",
|
|
2414
|
+
...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database", path11.join("lib", dirname))),
|
|
2415
|
+
...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service", path11.join("lib", dirname))),
|
|
2416
|
+
...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar", path11.join("lib/__scalar", dirname)))
|
|
2545
2417
|
]);
|
|
2546
2418
|
if (violations.length > 0) {
|
|
2547
2419
|
throw new Error(`[scan-convention]
|
|
2548
|
-
${violations.sort().map((violation) => `- ${violation}`).join(`
|
|
2420
|
+
${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
|
|
2549
2421
|
`)}`);
|
|
2550
2422
|
}
|
|
2551
2423
|
}
|
|
@@ -2553,11 +2425,11 @@ async function validateModuleFiles(exec, violations, kind, modulePath) {
|
|
|
2553
2425
|
const { files, dirs } = await exec.getFilesAndDirs(modulePath);
|
|
2554
2426
|
const moduleName = getModuleNameFromPath(kind, modulePath);
|
|
2555
2427
|
dirs.forEach((dirname) => {
|
|
2556
|
-
violations.push(`${getScanPath(exec,
|
|
2428
|
+
violations.push(`${getScanPath(exec, path11.join(modulePath, dirname))}: unsupported module folder`);
|
|
2557
2429
|
});
|
|
2558
2430
|
const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
|
|
2559
2431
|
for (const filename of files) {
|
|
2560
|
-
const scanPath = getScanPath(exec,
|
|
2432
|
+
const scanPath = getScanPath(exec, path11.join(modulePath, filename));
|
|
2561
2433
|
if (filename === "index.ts" || filename === "index.tsx")
|
|
2562
2434
|
continue;
|
|
2563
2435
|
if (filename === `${moduleName}.abstract.md`)
|
|
@@ -2632,16 +2504,16 @@ class ScanInfo {
|
|
|
2632
2504
|
}
|
|
2633
2505
|
};
|
|
2634
2506
|
await Promise.all([
|
|
2635
|
-
...databaseDirs.map((name) => collect(
|
|
2636
|
-
...serviceDirs.map((dirname) => collect(
|
|
2637
|
-
...scalarDirs.map((name) => collect(
|
|
2507
|
+
...databaseDirs.map((name) => collect(path11.join("lib", name), name, "databases")),
|
|
2508
|
+
...serviceDirs.map((dirname) => collect(path11.join("lib", dirname), dirname.slice(1), "services")),
|
|
2509
|
+
...scalarDirs.map((name) => collect(path11.join("lib/__scalar", name), name, "scalars"))
|
|
2638
2510
|
]);
|
|
2639
2511
|
const routes = exec.type === "lib" ? [] : await exec.getPageKeys();
|
|
2640
2512
|
const common = {
|
|
2641
2513
|
name: exec.name,
|
|
2642
2514
|
type: exec.type,
|
|
2643
2515
|
repoName: exec.workspace.repoName,
|
|
2644
|
-
serveDomain: WorkspaceExecutor.getBaseDevEnv(
|
|
2516
|
+
serveDomain: WorkspaceExecutor.getBaseDevEnv(path11.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
|
|
2645
2517
|
files,
|
|
2646
2518
|
libDeps,
|
|
2647
2519
|
pkgDeps,
|
|
@@ -2869,6 +2741,136 @@ class WorkspaceInfo {
|
|
|
2869
2741
|
}
|
|
2870
2742
|
}
|
|
2871
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
|
+
|
|
2872
2874
|
// pkgs/@akanjs/devkit/executors.ts
|
|
2873
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(" ")));
|
|
2874
2876
|
var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
|
|
@@ -3371,6 +3373,7 @@ class WorkspaceExecutor extends Executor {
|
|
|
3371
3373
|
return new WorkspaceExecutor({ workspaceRoot, repoName });
|
|
3372
3374
|
}
|
|
3373
3375
|
static publicEnvPin = null;
|
|
3376
|
+
static localServeDomain = "localhost";
|
|
3374
3377
|
static getBaseDevEnv(envPath) {
|
|
3375
3378
|
const sourceEnv = {
|
|
3376
3379
|
...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
|
|
@@ -3379,12 +3382,9 @@ class WorkspaceExecutor extends Executor {
|
|
|
3379
3382
|
const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
|
|
3380
3383
|
const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
|
|
3381
3384
|
const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
|
|
3382
|
-
const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
3386
|
-
if (!serveDomain)
|
|
3387
|
-
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]);
|
|
3388
3388
|
const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
|
|
3389
3389
|
const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
|
|
3390
3390
|
if (!env)
|
|
@@ -3396,9 +3396,18 @@ class WorkspaceExecutor extends Executor {
|
|
|
3396
3396
|
serveDomain,
|
|
3397
3397
|
env,
|
|
3398
3398
|
portOffset,
|
|
3399
|
-
workspaceId
|
|
3399
|
+
workspaceId,
|
|
3400
|
+
defaulted
|
|
3400
3401
|
};
|
|
3401
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
|
+
}
|
|
3402
3411
|
getWorkspaceId({
|
|
3403
3412
|
allowEmpty
|
|
3404
3413
|
} = {}) {
|
|
@@ -3775,10 +3784,12 @@ class AppExecutor extends SysExecutor {
|
|
|
3775
3784
|
return basePort + appIndex + portOffset;
|
|
3776
3785
|
}
|
|
3777
3786
|
getCommandEnv(env = {}) {
|
|
3778
|
-
const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
|
|
3787
|
+
const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
|
|
3779
3788
|
const PORT = (8282 + portOffset).toString();
|
|
3780
3789
|
return {
|
|
3781
3790
|
...process.env,
|
|
3791
|
+
AKAN_PUBLIC_REPO_NAME: repoName,
|
|
3792
|
+
AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
|
|
3782
3793
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
3783
3794
|
AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
|
|
3784
3795
|
NODE_NO_WARNINGS: "1",
|
|
@@ -3929,7 +3940,7 @@ class AppExecutor extends SysExecutor {
|
|
|
3929
3940
|
pageKeys.push(key);
|
|
3930
3941
|
}
|
|
3931
3942
|
}
|
|
3932
|
-
pageKeys.sort();
|
|
3943
|
+
pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3933
3944
|
this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
|
|
3934
3945
|
return this.#pageKeys;
|
|
3935
3946
|
}
|
|
@@ -4175,7 +4186,7 @@ class PkgExecutor extends Executor {
|
|
|
4175
4186
|
}
|
|
4176
4187
|
async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
|
|
4177
4188
|
const dependencyNames = [...new Set(dependencies)].sort();
|
|
4178
|
-
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);
|
|
4179
4190
|
const dependencyVersions = new Map;
|
|
4180
4191
|
const missingDeps = [];
|
|
4181
4192
|
for (const dep of [...dependencyNames, ...devDependencyNames]) {
|