@akanjs/cli 3.1.0-rc.2 → 3.1.0-rc.20
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-q50k4s45.js → CodeAgent-n3tzjm91.js} +2 -2
- package/{CodeAgentRpcHost-p7h6n2hp.js → CodeAgentRpcHost-9g27hmer.js} +1 -1
- package/{CodeTui-62g35ztk.js → CodeTui-fnm9ft4d.js} +47 -4
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-adfd5299.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +7 -5
- package/{application.command-hj3gv1rx.js → application.command-zfja7x4r.js} +12 -11
- package/{applicationBuildRunner-by10r9gf.js → applicationBuildRunner-vh4j6vdv.js} +16 -9
- package/buildBatch.proc.js +7 -7
- package/{cloud.command-k2fhaape.js → cloud.command-fcc8e8tp.js} +17 -16
- package/cloud.script-1xt3tnts.js +24 -0
- package/{code.command-kt5zgcc2.js → code.command-130bdmhe.js} +25 -12
- package/{context.command-8fgn42jw.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-3yxxr2mm.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +25 -10
- package/guidelines/conventions/conventions.instruction.md +463 -1084
- package/guidelines/cssRule/cssRule.instruction.md +16 -1
- package/guidelines/fieldRule/fieldRule.instruction.md +74 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +16 -9
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +16 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +47 -12
- package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +107 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +70 -7
- package/incrementalBuilder.proc.js +4 -4
- package/{index-nq045px5.js → index-192ha4xh.js} +356 -307
- package/{index-t7nws9mx.js → index-1djcjcre.js} +661 -448
- package/{index-h85cxfkg.js → index-1p98exm2.js} +2 -2
- package/{index-hnby7p9n.js → index-1xj2t7cb.js} +7 -7
- package/{index-wa5jbxj1.js → index-2mrn1mrg.js} +1 -0
- package/{index-58vvz603.js → index-35hc95h5.js} +1 -1
- package/{index-tv8fjaba.js → index-4mhy6qme.js} +4 -4
- package/{index-rk9sdys3.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/{index-kc8z134d.js → index-665zp9rs.js} +38 -15
- package/{index-jwqtchha.js → index-86jt2a0e.js} +1 -1
- package/{index-d2242ksy.js → index-at7bz0xq.js} +3 -0
- package/{index-sqfdmb5b.js → index-awbdfcf2.js} +4 -4
- package/{index-azh29dc8.js → index-axs61rna.js} +19 -14
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-6726qr1m.js → index-cpb3nvma.js} +57 -58
- package/{index-pyxn1b53.js → index-d72hfmdm.js} +13 -13
- package/{index-8zdwpxef.js → index-dye8pnxx.js} +119 -81
- package/{index-z507nzhg.js → index-fmzcs8v6.js} +4 -4
- package/{index-3reaaa1h.js → index-fyq1z6kt.js} +29 -27
- package/{index-0909wckt.js → index-g1mbppa9.js} +238 -236
- package/{index-fjsv10hd.js → index-hsadfb6c.js} +7 -7
- package/{index-tz5evhdr.js → index-j237xtew.js} +6 -6
- package/{index-nq2ja5e0.js → index-j4mkhz93.js} +3 -3
- package/{index-31ce9vhg.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-bea2gwc6.js → index-pydbnfez.js} +2 -2
- package/{index-20cebtth.js → index-qtfmtcrw.js} +17 -15
- package/{index-sby8fe1p.js → index-qwa5k1j8.js} +1 -1
- package/{index-e9prnmyt.js → index-tamvyvj3.js} +3 -3
- package/{index-rhbabmja.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-2etqq75t.js → index-va06nr10.js} +1090 -1078
- package/{index-g0vwzy9m.js → index-vk0pa0mn.js} +20 -21
- package/{index-xpdgxsj5.js → index-w44cy4vs.js} +2216 -2216
- package/index.js +22 -22
- package/{library.command-8tw59xzj.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-tf3dmznd.js → localRegistry.command-hj3p16p5.js} +15 -14
- package/{module.command-9vm8hffy.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-erbxe2tz.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-jpavcj0p.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-b8rzvjvv.js → pipeline.command-a2nrk26m.js} +3 -3
- package/{primitive.command-sm32chzk.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-jamtx95x.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-pkd17n6y.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8y4mq6hs.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-q8khmyx5.js → scalar.command-20b8cy0k.js} +4 -4
- package/skills/akan-code/SKILL.md +4 -0
- package/skills/akan-store/SKILL.md +2 -1
- package/{subspace.command-51n0ykbx.js → subspace.command-baz73jpk.js} +11 -10
- package/templates/appSample/lib/task/Task.Util.tsx +7 -7
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
- package/templates/appSample/ui/QuantityControl.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +23 -52
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{tunnel.command-7820vtxq.js → tunnel.command-2kw8vt9g.js} +8 -7
- package/{workflow.command-n5sv4wqz.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-zdy60jh8.js → workspace.command-jqs7vjhe.js} +29 -28
- package/TunnelShare-f1egrktx.js +0 -13
- package/cloud.script-z8p5dz7e.js +0 -23
- package/context.runner-hnh0rg7m.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,838 +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
|
-
|
|
555
|
-
${layoutSignature}({ children, params, searchParams }: LayoutProps) {
|
|
556
|
-
${layoutBinding} return `;
|
|
557
|
-
const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
558
|
-
import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
|
|
559
|
-
import type { ReactNode } from "react";
|
|
560
|
-
import { System } from "akanjs/ui";
|
|
561
|
-
import { env } from "@apps/${opts.appName}/env/env.client";
|
|
562
|
-
import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
|
|
563
|
-
${clientImport}${inheritedImport}${userImport}
|
|
564
|
-
const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
|
|
565
|
-
const defaultFonts = userFonts.filter((font) => font.default);
|
|
566
|
-
if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
|
|
567
|
-
const defaultFont = defaultFonts[0];
|
|
568
|
-
const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
|
|
569
|
-
export const theme = userLayout.theme ?? inheritedLayout.theme;
|
|
570
|
-
export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
|
|
571
|
-
export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
|
|
572
|
-
|
|
573
|
-
${layoutExports}(
|
|
574
|
-
<System.Provider
|
|
575
|
-
of={GeneratedLayout as never}
|
|
576
|
-
appName=${JSON.stringify(opts.appName)}
|
|
577
|
-
${prefix ? `prefix=${JSON.stringify(prefix)}
|
|
578
|
-
` : ""}params={params}
|
|
579
|
-
manifest={manifest}
|
|
580
|
-
env={env}
|
|
581
|
-
theme={theme}
|
|
582
|
-
viewport={viewport}
|
|
583
|
-
fonts={loadFonts(userFonts)}
|
|
584
|
-
className={defaultFontClassName}
|
|
585
|
-
layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
|
|
586
|
-
reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
|
|
587
|
-
wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
|
|
588
|
-
allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
|
|
589
|
-
>
|
|
590
|
-
${layoutChild}
|
|
591
|
-
</System.Provider>
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
|
|
595
|
-
import { resolveRouteModule, RouteHead } from "akanjs/client";
|
|
596
|
-
import type { ReactNode } from "react";
|
|
597
|
-
${inheritedImport}${userImport}
|
|
598
|
-
${layoutExports}${layoutReturn};
|
|
599
|
-
}
|
|
600
|
-
`;
|
|
601
|
-
await Bun.write(absPath, source);
|
|
602
|
-
return absPath;
|
|
603
|
-
}
|
|
604
|
-
async function resolveSsrPageEntries(opts) {
|
|
605
|
-
const absPageDir = path4.resolve(opts.appCwdPath, "page");
|
|
606
|
-
const hasSt = await Bun.file(path4.join(opts.appCwdPath, "lib", "st.ts")).exists();
|
|
607
|
-
const basePaths = opts.basePaths ?? [];
|
|
608
|
-
const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
|
|
609
|
-
const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
|
|
610
|
-
const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
|
|
611
|
-
const segments = getRootBoundarySegments(key);
|
|
612
|
-
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
613
|
-
}));
|
|
614
|
-
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
615
|
-
const userAbsPath = path4.resolve(absPageDir, key);
|
|
616
|
-
if (OVERRIDES_KEY_RE.test(key)) {
|
|
617
|
-
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
618
|
-
appCwdPath: opts.appCwdPath,
|
|
619
|
-
key,
|
|
620
|
-
userAbsPath
|
|
621
|
-
});
|
|
622
|
-
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
623
|
-
}
|
|
624
|
-
return { key, moduleAbsPath: userAbsPath };
|
|
625
|
-
}));
|
|
626
|
-
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
627
|
-
key: implicitRootLayoutKey(boundary.segments),
|
|
628
|
-
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
629
|
-
appCwdPath: opts.appCwdPath,
|
|
630
|
-
appName: opts.appName,
|
|
631
|
-
boundary,
|
|
632
|
-
rootSourceAbsPath,
|
|
633
|
-
includeStInit: hasSt && boundary.segments.length === 0,
|
|
634
|
-
includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
|
|
635
|
-
}),
|
|
636
|
-
seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
|
|
637
|
-
})));
|
|
638
|
-
const appCwdAbsPath = path4.resolve(opts.appCwdPath);
|
|
639
|
-
await Promise.all([
|
|
640
|
-
pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
|
|
641
|
-
pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
|
|
642
|
-
]);
|
|
643
|
-
return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
|
|
644
|
-
}
|
|
645
|
-
async function resolveSsrPageEntriesForApp(app, pageKeys) {
|
|
646
|
-
const config = await app.getConfig();
|
|
647
|
-
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// pkgs/@akanjs/devkit/codegenLock.ts
|
|
651
|
-
import { mkdir as mkdir2, open, readFile, rm as rm3, stat } from "fs/promises";
|
|
652
|
-
import path5 from "path";
|
|
653
|
-
import { Logger as Logger2 } from "akanjs/common";
|
|
654
|
-
|
|
655
|
-
class CodegenLock {
|
|
656
|
-
static fileName = "codegen.lock";
|
|
657
|
-
static waitTimeoutMs = 1e4;
|
|
658
|
-
static unknownHolderStaleMs = 60000;
|
|
659
|
-
static #pollMs = 25;
|
|
660
|
-
static #logger = new Logger2("CodegenLock");
|
|
661
|
-
static #queue = Promise.resolve();
|
|
662
|
-
static pathIn(workspaceRoot) {
|
|
663
|
-
return path5.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
|
|
664
|
-
}
|
|
665
|
-
static async run(workspaceRoot, label, fn) {
|
|
666
|
-
const ahead = CodegenLock.#queue;
|
|
667
|
-
let done;
|
|
668
|
-
CodegenLock.#queue = new Promise((resolve) => {
|
|
669
|
-
done = resolve;
|
|
670
|
-
});
|
|
671
|
-
try {
|
|
672
|
-
await CodegenLock.#waitForQueue(ahead, label);
|
|
673
|
-
return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
|
|
674
|
-
} finally {
|
|
675
|
-
done();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
static async#waitForQueue(ahead, label) {
|
|
679
|
-
let timer = null;
|
|
680
|
-
const expired = new Promise((resolve) => {
|
|
681
|
-
timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
|
|
682
|
-
});
|
|
683
|
-
try {
|
|
684
|
-
if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
|
|
685
|
-
CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
|
|
686
|
-
} finally {
|
|
687
|
-
if (timer)
|
|
688
|
-
clearTimeout(timer);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
static async#withFileLock(workspaceRoot, label, fn) {
|
|
692
|
-
const lockPath = CodegenLock.pathIn(workspaceRoot);
|
|
693
|
-
await mkdir2(path5.dirname(lockPath), { recursive: true }).catch(() => {
|
|
694
|
-
return;
|
|
695
|
-
});
|
|
696
|
-
const held = await CodegenLock.#acquire(lockPath, label);
|
|
697
|
-
try {
|
|
698
|
-
return await fn();
|
|
699
|
-
} finally {
|
|
700
|
-
if (held)
|
|
701
|
-
await rm3(lockPath, { force: true }).catch(() => {
|
|
702
|
-
return;
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
static async#acquire(lockPath, label) {
|
|
707
|
-
const deadline = Date.now() + CodegenLock.waitTimeoutMs;
|
|
708
|
-
for (;; ) {
|
|
709
|
-
const handle = await open(lockPath, "wx").catch(() => null);
|
|
710
|
-
if (handle) {
|
|
711
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
|
|
712
|
-
return;
|
|
713
|
-
});
|
|
714
|
-
await handle.close().catch(() => {
|
|
715
|
-
return;
|
|
716
|
-
});
|
|
717
|
-
return true;
|
|
718
|
-
}
|
|
719
|
-
if (await CodegenLock.#reclaimIfAbandoned(lockPath))
|
|
720
|
-
continue;
|
|
721
|
-
if (Date.now() >= deadline) {
|
|
722
|
-
CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
await Bun.sleep(CodegenLock.#pollMs);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
static async#reclaimIfAbandoned(lockPath) {
|
|
729
|
-
const info = await stat(lockPath).catch(() => null);
|
|
730
|
-
if (!info)
|
|
731
|
-
return true;
|
|
732
|
-
const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
|
|
733
|
-
if (holder) {
|
|
734
|
-
if (CodegenLock.#isAlive(holder.pid))
|
|
735
|
-
return false;
|
|
736
|
-
} else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
|
|
737
|
-
return false;
|
|
738
|
-
await rm3(lockPath, { force: true }).catch(() => {
|
|
739
|
-
return;
|
|
740
|
-
});
|
|
741
|
-
return true;
|
|
742
|
-
}
|
|
743
|
-
static #parseHolder(raw) {
|
|
744
|
-
try {
|
|
745
|
-
const parsed = JSON.parse(raw);
|
|
746
|
-
if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
|
|
747
|
-
return null;
|
|
748
|
-
return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
|
|
749
|
-
} catch {
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
static #isAlive(pid) {
|
|
754
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
755
|
-
return false;
|
|
756
|
-
try {
|
|
757
|
-
process.kill(pid, 0);
|
|
758
|
-
return true;
|
|
759
|
-
} catch (error) {
|
|
760
|
-
return error.code === "EPERM";
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// pkgs/@akanjs/devkit/spinner.ts
|
|
766
|
-
import ora from "ora";
|
|
767
|
-
|
|
768
|
-
class Spinner {
|
|
769
|
-
static padding = 12;
|
|
770
|
-
static oraOptions = { discardStdin: false };
|
|
771
|
-
static canAnimate(stream = process.stderr) {
|
|
772
|
-
return !stream.isTTY || stream.columns > 0;
|
|
773
|
-
}
|
|
774
|
-
spinner;
|
|
775
|
-
stopWatch = null;
|
|
776
|
-
startAt = new Date;
|
|
777
|
-
prefix;
|
|
778
|
-
message;
|
|
779
|
-
enableSpin;
|
|
780
|
-
constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
|
|
781
|
-
Spinner.padding = Math.max(Spinner.padding, prefix.length);
|
|
782
|
-
this.prefix = prefix;
|
|
783
|
-
this.message = message;
|
|
784
|
-
this.spinner = ora({ ...Spinner.oraOptions, text: message });
|
|
785
|
-
this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
|
|
786
|
-
this.spinner.indent = indent;
|
|
787
|
-
this.enableSpin = enableSpin && Spinner.canAnimate();
|
|
788
|
-
}
|
|
789
|
-
start() {
|
|
790
|
-
this.startAt = new Date;
|
|
791
|
-
if (this.enableSpin) {
|
|
792
|
-
this.spinner.start();
|
|
793
|
-
this.stopWatch = setInterval(() => {
|
|
794
|
-
this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
|
|
795
|
-
this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
|
|
796
|
-
}, 1000);
|
|
797
|
-
} else
|
|
798
|
-
this.spinner.info();
|
|
799
|
-
return this;
|
|
800
|
-
}
|
|
801
|
-
succeed(message) {
|
|
802
|
-
this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
|
|
803
|
-
this.#reset();
|
|
804
|
-
}
|
|
805
|
-
fail(message) {
|
|
806
|
-
this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
|
|
807
|
-
this.#reset();
|
|
808
|
-
}
|
|
809
|
-
isSpinning() {
|
|
810
|
-
return this.spinner.isSpinning;
|
|
811
|
-
}
|
|
812
|
-
#reset() {
|
|
813
|
-
if (this.stopWatch)
|
|
814
|
-
clearInterval(this.stopWatch);
|
|
815
|
-
this.stopWatch = null;
|
|
816
|
-
}
|
|
817
|
-
#getElapsedTimeStr({ floor = false } = {}) {
|
|
818
|
-
const ms = Date.now() - this.startAt.getTime();
|
|
819
|
-
if (ms < 1000)
|
|
820
|
-
return `${ms}ms`;
|
|
821
|
-
const s = Math.floor(ms / 1000);
|
|
822
|
-
if (s < 60)
|
|
823
|
-
return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
|
|
824
|
-
const m = Math.floor(s / 60);
|
|
825
|
-
return `${m}m ${s % 60}s`;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// pkgs/@akanjs/devkit/executors.ts
|
|
830
|
-
import {
|
|
831
|
-
exec,
|
|
832
|
-
fork,
|
|
833
|
-
spawn as spawn2
|
|
834
|
-
} from "child_process";
|
|
835
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
836
|
-
import {
|
|
837
|
-
copyFile,
|
|
838
|
-
cp as cpEntry,
|
|
839
|
-
lstat,
|
|
840
|
-
mkdir as mkdir3,
|
|
841
|
-
readdir as readDirEntries,
|
|
842
|
-
realpath,
|
|
843
|
-
rm as rm4,
|
|
844
|
-
stat as stat2,
|
|
845
|
-
symlink
|
|
846
|
-
} from "fs/promises";
|
|
847
|
-
import path13 from "path";
|
|
848
|
-
import { pathToFileURL } from "url";
|
|
849
|
-
import {
|
|
850
|
-
capitalize,
|
|
851
|
-
getPageSourceFileViolation,
|
|
852
|
-
isRouteSourceFile,
|
|
853
|
-
Logger as Logger3,
|
|
854
|
-
parseRouteModuleKey,
|
|
855
|
-
toError as toError2,
|
|
856
|
-
validatePageSourceFile,
|
|
857
|
-
validateSubRoutePageKey
|
|
858
|
-
} from "akanjs/common";
|
|
859
|
-
var {$ } = globalThis.Bun;
|
|
860
|
-
import chalk2 from "chalk";
|
|
861
|
-
|
|
862
|
-
// pkgs/@akanjs/devkit/agentsIndex.ts
|
|
863
|
-
var recipeScannerLoad = null;
|
|
864
|
-
var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
|
|
865
|
-
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
866
|
-
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
867
|
-
var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
868
|
-
var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
|
|
869
|
-
|
|
870
|
-
${block}`;
|
|
871
|
-
var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
872
|
-
var readDevkitVersion = async () => {
|
|
873
|
-
const { readFile } = await import("fs/promises");
|
|
874
|
-
const { getDirname } = await import("./fileSys-wzzrntkw.js");
|
|
875
|
-
try {
|
|
876
|
-
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
877
|
-
return JSON.parse(raw).version ?? null;
|
|
878
|
-
} catch {
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
var upsertAgentBlock = (existing, block) => {
|
|
883
|
-
const managed = `${AGENT_BLOCK_START}
|
|
884
|
-
${block}
|
|
885
|
-
${AGENT_BLOCK_END}`;
|
|
886
|
-
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
887
|
-
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
888
|
-
if (startIndex >= 0 && endIndex > startIndex) {
|
|
889
|
-
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
890
|
-
}
|
|
891
|
-
return `${existing.replace(/\s*$/, "")}
|
|
892
|
-
|
|
893
|
-
${managed}
|
|
894
|
-
`;
|
|
895
|
-
};
|
|
896
|
-
var extractAgentBlock = (content) => {
|
|
897
|
-
const startIndex = content.indexOf(AGENT_BLOCK_START);
|
|
898
|
-
const endIndex = content.indexOf(AGENT_BLOCK_END);
|
|
899
|
-
if (startIndex < 0 || endIndex <= startIndex)
|
|
900
|
-
return null;
|
|
901
|
-
return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
|
|
902
|
-
};
|
|
903
|
-
var signatureOf = (recipe) => {
|
|
904
|
-
const entries = Object.entries(recipe.variants);
|
|
905
|
-
if (entries.length === 0)
|
|
906
|
-
return "";
|
|
907
|
-
const parts = entries.map(([key, values]) => {
|
|
908
|
-
if (values.length === 1 && values[0] === "true")
|
|
909
|
-
return `${key}?`;
|
|
910
|
-
const def = recipe.defaultVariants?.[key];
|
|
911
|
-
return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
|
|
912
|
-
});
|
|
913
|
-
return `(${parts.join(" \xB7 ")})`;
|
|
914
|
-
};
|
|
915
|
-
var renderRecipeEntries = (recipes) => {
|
|
916
|
-
const groups = new Map;
|
|
917
|
-
for (const recipe of recipes)
|
|
918
|
-
groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
|
|
919
|
-
return [...groups.entries()].map(([importFrom, list]) => {
|
|
920
|
-
const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
|
|
921
|
-
`);
|
|
922
|
-
return `Import from \`${importFrom}\`:
|
|
923
|
-
${items}`;
|
|
924
|
-
}).join(`
|
|
925
|
-
|
|
926
|
-
`);
|
|
927
|
-
};
|
|
928
|
-
var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
|
|
929
|
-
const { collectRecipeSources } = await loadRecipeScanner();
|
|
930
|
-
const sources = [
|
|
931
|
-
...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
|
|
932
|
-
];
|
|
933
|
-
for (const lib of [...new Set(libDeps)].sort()) {
|
|
934
|
-
if (scope.type === "lib" && lib === scope.name)
|
|
935
|
-
continue;
|
|
936
|
-
sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
|
|
937
|
-
}
|
|
938
|
-
return sources;
|
|
939
|
-
};
|
|
940
|
-
var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
|
|
941
|
-
const { scanRecipes } = await loadRecipeScanner();
|
|
942
|
-
return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
|
|
943
|
-
};
|
|
944
|
-
var renderScopeAgentBlock = (scope, recipes) => {
|
|
945
|
-
const scopePath = `${scope.type}s/${scope.name}`;
|
|
946
|
-
const intro = `## Recipes In Scope
|
|
947
|
-
|
|
948
|
-
UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
|
|
949
|
-
\`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
|
|
950
|
-
arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
|
|
951
|
-
boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
|
|
952
|
-
and verified by \`akan lint ${scope.name}\`.`;
|
|
953
|
-
if (recipes.length === 0) {
|
|
954
|
-
return `${intro}
|
|
955
|
-
|
|
956
|
-
No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
|
|
957
|
-
root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
|
|
958
|
-
folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
|
|
959
|
-
}
|
|
960
|
-
return `${intro}
|
|
961
|
-
|
|
962
|
-
${renderRecipeEntries(recipes)}`;
|
|
963
|
-
};
|
|
964
|
-
var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
|
|
965
|
-
|
|
966
|
-
Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
|
|
967
|
-
root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
|
|
968
|
-
section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
|
|
969
|
-
markers freely.
|
|
970
|
-
|
|
971
|
-
${AGENT_BLOCK_START}
|
|
972
|
-
${block}
|
|
973
|
-
${AGENT_BLOCK_END}
|
|
974
|
-
`;
|
|
975
|
-
var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
|
|
976
|
-
|
|
977
|
-
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
978
|
-
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
979
|
-
|
|
980
|
-
Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
|
|
981
|
-
|
|
982
|
-
- Restates the identifier, the signature, or the line under it \u2192 delete it.
|
|
983
|
-
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
|
|
984
|
-
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
|
|
985
|
-
- Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
|
|
986
|
-
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
987
|
-
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
|
|
988
|
-
keep it, one line.
|
|
989
|
-
|
|
990
|
-
That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
991
|
-
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
992
|
-
surrounding code carries none, your diff carries none.`;
|
|
993
|
-
var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
|
|
994
|
-
|
|
995
|
-
@AGENTS.md
|
|
996
|
-
|
|
997
|
-
${CLAUDE_COMMENT_RULE}
|
|
998
|
-
`;
|
|
999
|
-
|
|
1000
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
1001
|
-
import path6 from "path";
|
|
1002
|
-
|
|
1003
|
-
// pkgs/@akanjs/devkit/akanConfig/types.ts
|
|
1004
|
-
import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
|
|
1005
|
-
|
|
1006
|
-
// pkgs/@akanjs/devkit/akanConfig/akanBin.ts
|
|
1007
|
-
class AkanBin {
|
|
1008
|
-
static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
|
|
1009
|
-
static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1010
|
-
static parse(declared, owner, baseDir) {
|
|
1011
|
-
if (declared === undefined)
|
|
1012
|
-
return {};
|
|
1013
|
-
if (!declared || typeof declared !== "object" || Array.isArray(declared))
|
|
1014
|
-
throw new Error(`${owner}: bin maps each executable's name to its platforms`);
|
|
1015
|
-
return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
|
|
1016
|
-
}
|
|
1017
|
-
static isArchive(source) {
|
|
1018
|
-
return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
|
|
1019
|
-
}
|
|
1020
|
-
static #platforms(name, platforms, owner, baseDir) {
|
|
1021
|
-
if (!AkanBin.fileName.test(name))
|
|
1022
|
-
throw new Error(`${owner}: bin.${name} is not a file name`);
|
|
1023
|
-
if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
|
|
1024
|
-
throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
|
|
1025
|
-
const resolved = {};
|
|
1026
|
-
for (const [platform, source] of Object.entries(platforms)) {
|
|
1027
|
-
if (!binPlatforms.includes(platform))
|
|
1028
|
-
throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
|
|
1029
|
-
resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
|
|
1030
|
-
}
|
|
1031
|
-
return resolved;
|
|
1032
|
-
}
|
|
1033
|
-
static #source(at, source, baseDir) {
|
|
1034
|
-
if (!source || typeof source !== "object" || Array.isArray(source))
|
|
1035
|
-
throw new Error(`${at} must be { url, sha256 } or { path }`);
|
|
1036
|
-
const { url, sha256, path: localPath, file, ...rest } = source;
|
|
1037
|
-
const extra = Object.keys(rest);
|
|
1038
|
-
if (extra.length)
|
|
1039
|
-
throw new Error(`${at}: unknown key ${extra.join(", ")}`);
|
|
1040
|
-
if (url === undefined === (localPath === undefined))
|
|
1041
|
-
throw new Error(`${at} takes either url and sha256, or path`);
|
|
1042
|
-
const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
|
|
1043
|
-
const archive = AkanBin.isArchive(resolved);
|
|
1044
|
-
if (file === undefined) {
|
|
1045
|
-
if (archive)
|
|
1046
|
-
throw new Error(`${at} names an archive: file picks the executable inside it`);
|
|
1047
|
-
return resolved;
|
|
1048
|
-
}
|
|
1049
|
-
if (!archive)
|
|
1050
|
-
throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
|
|
1051
|
-
const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
|
|
1052
|
-
if (!parts.length || path6.isAbsolute(file) || parts.includes(".."))
|
|
1053
|
-
throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
|
|
1054
|
-
return { ...resolved, file: parts.join("/") };
|
|
1055
|
-
}
|
|
1056
|
-
static #urlSource(at, url, sha256) {
|
|
1057
|
-
if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
|
|
1058
|
-
throw new Error(`${at}: url must be an http(s) address`);
|
|
1059
|
-
if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
|
|
1060
|
-
throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
|
|
1061
|
-
return { url, sha256: sha256.toLowerCase() };
|
|
1062
|
-
}
|
|
1063
|
-
static #pathSource(at, localPath, sha256, baseDir) {
|
|
1064
|
-
if (typeof localPath !== "string" || !localPath)
|
|
1065
|
-
throw new Error(`${at}: path must name a file`);
|
|
1066
|
-
if (sha256 !== undefined)
|
|
1067
|
-
throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
|
|
1068
|
-
return { path: path6.resolve(baseDir, localPath) };
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
// pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
|
|
1072
|
-
import fs2 from "fs";
|
|
1073
|
-
import path7 from "path";
|
|
1074
|
-
import { fileURLToPath } from "url";
|
|
1075
|
-
import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
|
|
1076
|
-
import { resolveAkanI18nConfig } from "akanjs/common";
|
|
1077
|
-
var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
1078
|
-
var DEFAULT_OPTIMIZE_IMPORTS = [
|
|
1079
|
-
"lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
|
|
1080
|
-
"@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
|
|
1081
|
-
"@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
|
|
1082
|
-
"@material-ui/icons @tabler/icons-react mui-core react-icons/*"
|
|
1083
|
-
].flatMap((line) => line.split(" "));
|
|
1084
|
-
var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
|
|
1085
|
-
var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
1086
|
-
var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
|
|
1087
|
-
var DEFAULT_AKAN_IMAGE_CONFIG = {
|
|
1088
|
-
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
|
1089
|
-
imageSizes: [32, 48, 64, 96, 128, 256, 384],
|
|
1090
|
-
formats: ["image/webp"],
|
|
1091
|
-
qualities: [75],
|
|
1092
|
-
minimumCacheTTL: 14400,
|
|
1093
|
-
remotePatterns: [],
|
|
1094
|
-
localPatterns: [{ pathname: "/**" }],
|
|
1095
|
-
dangerouslyAllowSVG: false,
|
|
1096
|
-
maximumRedirects: 3,
|
|
1097
|
-
fetchTimeoutMs: 7000,
|
|
1098
|
-
maxRemoteBytes: 25 * 1024 * 1024,
|
|
1099
|
-
maxConcurrency: 0
|
|
1100
|
-
};
|
|
1101
|
-
var normalizeIndexPath = (indexPath) => {
|
|
1102
|
-
const normalized = indexPath?.trim();
|
|
1103
|
-
if (!normalized)
|
|
1104
|
-
return;
|
|
1105
|
-
const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
|
|
1106
|
-
return path === "/" ? "/" : path;
|
|
1107
|
-
};
|
|
1108
|
-
var normalizeStringList = (values) => {
|
|
1109
|
-
const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
1110
|
-
return normalized.length > 0 ? [...new Set(normalized)] : undefined;
|
|
1111
|
-
};
|
|
1112
|
-
var sanitizeAppIdSegment = (value) => {
|
|
1113
|
-
const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1114
|
-
if (!cleaned)
|
|
1115
|
-
return "app";
|
|
1116
|
-
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
|
|
1117
|
-
};
|
|
1118
|
-
var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
|
|
1119
|
-
var normalizeDeepLinkDomain = (domain) => {
|
|
1120
|
-
const normalized = domain.trim();
|
|
1121
|
-
if (!normalized)
|
|
1122
|
-
return "";
|
|
1123
|
-
try {
|
|
1124
|
-
const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
|
|
1125
|
-
return url.host.toLowerCase();
|
|
1126
|
-
} catch {
|
|
1127
|
-
return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
var normalizeDeepLinks = (deepLinks) => {
|
|
1131
|
-
const schemes = normalizeStringList(deepLinks?.schemes);
|
|
1132
|
-
const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
|
|
1133
|
-
if (!schemes && !domains)
|
|
1134
|
-
return;
|
|
1135
|
-
return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
|
|
1136
|
-
};
|
|
1137
|
-
var emptyLibContributions = () => ({
|
|
1138
|
-
externalLibs: [],
|
|
1139
|
-
trustedDependencies: [],
|
|
1140
|
-
bin: [],
|
|
1141
|
-
docker: { preRuns: [], postRuns: [] },
|
|
1142
|
-
keepFonts: []
|
|
1143
|
-
});
|
|
1144
|
-
var normalizePackageNames = (names, owner) => {
|
|
1145
|
-
if (names === undefined)
|
|
1146
|
-
return [];
|
|
1147
|
-
if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
|
|
1148
|
-
throw new Error(`${owner}: trustedDependencies lists package names`);
|
|
1149
|
-
return [...new Set(names.map((name) => name.trim()))];
|
|
1150
|
-
};
|
|
1151
|
-
var normalizeKeepFonts = (keepFonts) => [
|
|
1152
|
-
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
|
|
1153
|
-
];
|
|
1154
|
-
var dedupeDockerRuns = (runs) => {
|
|
1155
|
-
const byKey = new Map;
|
|
1156
|
-
for (const run of runs)
|
|
1157
|
-
byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
1158
|
-
return [...byKey.values()];
|
|
1159
|
-
};
|
|
1160
|
-
|
|
1161
|
-
class LibConfigRefusal extends Error {
|
|
598
|
+
class LibConfigRefusal extends Error {
|
|
1162
599
|
}
|
|
1163
600
|
|
|
1164
601
|
class AkanAppConfig {
|
|
@@ -1495,7 +932,7 @@ class AkanAppConfig {
|
|
|
1495
932
|
}
|
|
1496
933
|
#applyRoutes(routes = []) {
|
|
1497
934
|
for (const route of routes) {
|
|
1498
|
-
const basePath = route.basePath ? route.basePath
|
|
935
|
+
const basePath = route.basePath ? trimSlashes(route.basePath) : null;
|
|
1499
936
|
if (basePath !== null)
|
|
1500
937
|
this.basePaths.add(basePath);
|
|
1501
938
|
const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
|
|
@@ -1597,7 +1034,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
1597
1034
|
static async from(app, { bustImportCache = false } = {}) {
|
|
1598
1035
|
const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
|
|
1599
1036
|
AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
|
|
1600
|
-
WorkspaceExecutor.getBaseDevEnv(
|
|
1037
|
+
WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
|
|
1601
1038
|
app.workspace.getLibs(),
|
|
1602
1039
|
app.workspace.getPackageJson()
|
|
1603
1040
|
]);
|
|
@@ -1693,143 +1130,579 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
1693
1130
|
};
|
|
1694
1131
|
}
|
|
1695
1132
|
}
|
|
1696
|
-
var akanPackageJson = null;
|
|
1697
|
-
function getAkanPackageJson() {
|
|
1698
|
-
if (akanPackageJson)
|
|
1699
|
-
return akanPackageJson;
|
|
1700
|
-
const sourceDir =
|
|
1701
|
-
const packageJsonPaths = [
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
];
|
|
1706
|
-
try {
|
|
1707
|
-
packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
|
|
1708
|
-
} catch {}
|
|
1709
|
-
for (const packageJsonPath of packageJsonPaths) {
|
|
1710
|
-
try {
|
|
1711
|
-
akanPackageJson = JSON.parse(
|
|
1712
|
-
return akanPackageJson;
|
|
1713
|
-
} 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`);
|
|
1714
1430
|
}
|
|
1715
|
-
akanPackageJson = {
|
|
1716
|
-
name: "akanjs",
|
|
1717
|
-
version: "0.0.0",
|
|
1718
|
-
description: "akanjs",
|
|
1719
|
-
dependencies: {}
|
|
1720
|
-
};
|
|
1721
|
-
return akanPackageJson;
|
|
1722
1431
|
}
|
|
1723
|
-
function
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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);
|
|
1734
1481
|
}
|
|
1735
1482
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
+
}
|
|
1753
1623
|
}
|
|
1754
|
-
static async
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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
|
+
}
|
|
1763
1638
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
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
|
+
}
|
|
1786
1660
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
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;
|
|
1802
1675
|
}
|
|
1803
|
-
static #
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
return refuse("that is not an object");
|
|
1811
|
-
const unknown = Object.keys(cloud).filter((key) => key !== "host");
|
|
1812
|
-
if (unknown.length)
|
|
1813
|
-
return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
|
|
1814
|
-
const { host } = cloud;
|
|
1815
|
-
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 {
|
|
1816
1683
|
return null;
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (
|
|
1821
|
-
return
|
|
1822
|
-
|
|
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
|
+
}
|
|
1823
1695
|
}
|
|
1824
1696
|
}
|
|
1697
|
+
|
|
1825
1698
|
// pkgs/@akanjs/devkit/jsonFormatter.ts
|
|
1826
|
-
import
|
|
1699
|
+
import path8 from "path";
|
|
1827
1700
|
|
|
1828
1701
|
class JsonFormatter {
|
|
1829
1702
|
static lineWidth = 120;
|
|
1830
1703
|
static stringify(value, filePath) {
|
|
1831
1704
|
const json = JSON.stringify(value, null, 2);
|
|
1832
|
-
if (
|
|
1705
|
+
if (path8.basename(filePath) === "package.json")
|
|
1833
1706
|
return `${json}
|
|
1834
1707
|
`;
|
|
1835
1708
|
return `${JsonFormatter.#print(JSON.parse(json), 0, { column: 0, trailing: 0 })}
|
|
@@ -1900,7 +1773,7 @@ ${" ".repeat(depth)}]`;
|
|
|
1900
1773
|
// pkgs/@akanjs/devkit/linter.ts
|
|
1901
1774
|
import { spawn } from "child_process";
|
|
1902
1775
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1903
|
-
import
|
|
1776
|
+
import path9 from "path";
|
|
1904
1777
|
import chalk from "chalk";
|
|
1905
1778
|
var parseBiomeReport = (output) => {
|
|
1906
1779
|
for (let start = output.indexOf("{");start !== -1; start = output.indexOf("{", start + 1)) {
|
|
@@ -1966,13 +1839,13 @@ class Linter {
|
|
|
1966
1839
|
#biomeBin;
|
|
1967
1840
|
constructor(cwdPath) {
|
|
1968
1841
|
this.lintRoot = this.#findBiomeRootPath(cwdPath);
|
|
1969
|
-
this.configPath = Linter.#configPathIn(this.lintRoot) ??
|
|
1970
|
-
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);
|
|
1971
1844
|
this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
|
|
1972
1845
|
}
|
|
1973
1846
|
static #configPathIn(dir) {
|
|
1974
1847
|
for (const fileName of BIOME_CONFIG_FILES) {
|
|
1975
|
-
const configPath =
|
|
1848
|
+
const configPath = path9.join(dir, fileName);
|
|
1976
1849
|
if (existsSync2(configPath))
|
|
1977
1850
|
return configPath;
|
|
1978
1851
|
}
|
|
@@ -1981,19 +1854,19 @@ class Linter {
|
|
|
1981
1854
|
#findBiomeRootPath(dir) {
|
|
1982
1855
|
if (Linter.#configPathIn(dir))
|
|
1983
1856
|
return dir;
|
|
1984
|
-
const parentDir =
|
|
1857
|
+
const parentDir = path9.dirname(dir);
|
|
1985
1858
|
if (parentDir === dir)
|
|
1986
1859
|
throw new Error(`biome.json not found from ${dir}`);
|
|
1987
1860
|
return this.#findBiomeRootPath(parentDir);
|
|
1988
1861
|
}
|
|
1989
1862
|
#toBiomePath(filePath) {
|
|
1990
|
-
const relativePath =
|
|
1991
|
-
if (!relativePath.startsWith("..") && !
|
|
1863
|
+
const relativePath = path9.relative(this.lintRoot, filePath);
|
|
1864
|
+
if (!relativePath.startsWith("..") && !path9.isAbsolute(relativePath))
|
|
1992
1865
|
return relativePath;
|
|
1993
1866
|
return filePath;
|
|
1994
1867
|
}
|
|
1995
1868
|
#resolveFilePath(filePath) {
|
|
1996
|
-
return
|
|
1869
|
+
return path9.isAbsolute(filePath) ? filePath : path9.join(this.lintRoot, filePath);
|
|
1997
1870
|
}
|
|
1998
1871
|
async#runBiome(args, input) {
|
|
1999
1872
|
return await new Promise((resolve, reject) => {
|
|
@@ -2018,7 +1891,7 @@ class Linter {
|
|
|
2018
1891
|
const diagnosticPath = diagnostic.location?.path;
|
|
2019
1892
|
if (!diagnosticPath)
|
|
2020
1893
|
return fallbackFilePath;
|
|
2021
|
-
return
|
|
1894
|
+
return path9.isAbsolute(diagnosticPath) ? diagnosticPath : path9.join(this.lintRoot, diagnosticPath);
|
|
2022
1895
|
}
|
|
2023
1896
|
#createLintMessage(diagnostic) {
|
|
2024
1897
|
const start = diagnostic.location?.start;
|
|
@@ -2354,7 +2227,7 @@ class ManifestDependencies {
|
|
|
2354
2227
|
|
|
2355
2228
|
// pkgs/@akanjs/devkit/repoIdentity.ts
|
|
2356
2229
|
import { execFileSync } from "child_process";
|
|
2357
|
-
import
|
|
2230
|
+
import path10 from "path";
|
|
2358
2231
|
var resolved = new Map;
|
|
2359
2232
|
var readRemoteName = (workspaceRoot) => {
|
|
2360
2233
|
try {
|
|
@@ -2372,13 +2245,13 @@ var resolveRepoName = (workspaceRoot) => {
|
|
|
2372
2245
|
const cached = resolved.get(workspaceRoot);
|
|
2373
2246
|
if (cached)
|
|
2374
2247
|
return cached;
|
|
2375
|
-
const repoName = readRemoteName(workspaceRoot) ??
|
|
2248
|
+
const repoName = readRemoteName(workspaceRoot) ?? path10.basename(workspaceRoot);
|
|
2376
2249
|
resolved.set(workspaceRoot, repoName);
|
|
2377
2250
|
return repoName;
|
|
2378
2251
|
};
|
|
2379
2252
|
|
|
2380
2253
|
// pkgs/@akanjs/devkit/scanInfo.ts
|
|
2381
|
-
import
|
|
2254
|
+
import path11 from "path";
|
|
2382
2255
|
|
|
2383
2256
|
// pkgs/@akanjs/devkit/workspaceLayout.ts
|
|
2384
2257
|
var appRootAllowedFiles = new Set([
|
|
@@ -2499,10 +2372,10 @@ var moduleUiFileTypes = {
|
|
|
2499
2372
|
service: new Set(["Util", "Zone"]),
|
|
2500
2373
|
scalar: new Set(["Template", "Unit"])
|
|
2501
2374
|
};
|
|
2502
|
-
var createDependencyScanner = async (exec) => (await import("./dependencyScanner-
|
|
2503
|
-
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("/"));
|
|
2504
2377
|
var getModuleNameFromPath = (kind, modulePath) => {
|
|
2505
|
-
const dirname =
|
|
2378
|
+
const dirname = path11.basename(modulePath);
|
|
2506
2379
|
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
2507
2380
|
};
|
|
2508
2381
|
async function assertScanConvention(exec, libRoot) {
|
|
@@ -2523,28 +2396,28 @@ async function assertScanConvention(exec, libRoot) {
|
|
|
2523
2396
|
for (const filename of await exec.readdir(facet)) {
|
|
2524
2397
|
const shadow = facetIndexShadowOf(`${facet}/${filename}`);
|
|
2525
2398
|
if (shadow)
|
|
2526
|
-
addViolation(
|
|
2399
|
+
addViolation(path11.join(facet, filename), shadow);
|
|
2527
2400
|
}
|
|
2528
2401
|
if (exec.type === "lib")
|
|
2529
2402
|
for (const { relativePath, reason } of await exec.getPageConventionViolations())
|
|
2530
2403
|
addViolation(relativePath, reason);
|
|
2531
2404
|
for (const filename of libRoot.files)
|
|
2532
2405
|
if (!isAllowedLibFacetRootFile(filename))
|
|
2533
|
-
addViolation(
|
|
2406
|
+
addViolation(path11.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
|
|
2534
2407
|
for (const dirname of libRoot.dirs)
|
|
2535
2408
|
if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
|
|
2536
|
-
addViolation(
|
|
2409
|
+
addViolation(path11.join("lib", dirname), "unsupported internal lib folder");
|
|
2537
2410
|
const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
|
|
2538
2411
|
const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
|
|
2539
2412
|
const scalarDirs = await exec.readdir("lib/__scalar");
|
|
2540
2413
|
await Promise.all([
|
|
2541
|
-
...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database",
|
|
2542
|
-
...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service",
|
|
2543
|
-
...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)))
|
|
2544
2417
|
]);
|
|
2545
2418
|
if (violations.length > 0) {
|
|
2546
2419
|
throw new Error(`[scan-convention]
|
|
2547
|
-
${violations.sort().map((violation) => `- ${violation}`).join(`
|
|
2420
|
+
${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
|
|
2548
2421
|
`)}`);
|
|
2549
2422
|
}
|
|
2550
2423
|
}
|
|
@@ -2552,11 +2425,11 @@ async function validateModuleFiles(exec, violations, kind, modulePath) {
|
|
|
2552
2425
|
const { files, dirs } = await exec.getFilesAndDirs(modulePath);
|
|
2553
2426
|
const moduleName = getModuleNameFromPath(kind, modulePath);
|
|
2554
2427
|
dirs.forEach((dirname) => {
|
|
2555
|
-
violations.push(`${getScanPath(exec,
|
|
2428
|
+
violations.push(`${getScanPath(exec, path11.join(modulePath, dirname))}: unsupported module folder`);
|
|
2556
2429
|
});
|
|
2557
2430
|
const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
|
|
2558
2431
|
for (const filename of files) {
|
|
2559
|
-
const scanPath = getScanPath(exec,
|
|
2432
|
+
const scanPath = getScanPath(exec, path11.join(modulePath, filename));
|
|
2560
2433
|
if (filename === "index.ts" || filename === "index.tsx")
|
|
2561
2434
|
continue;
|
|
2562
2435
|
if (filename === `${moduleName}.abstract.md`)
|
|
@@ -2631,16 +2504,16 @@ class ScanInfo {
|
|
|
2631
2504
|
}
|
|
2632
2505
|
};
|
|
2633
2506
|
await Promise.all([
|
|
2634
|
-
...databaseDirs.map((name) => collect(
|
|
2635
|
-
...serviceDirs.map((dirname) => collect(
|
|
2636
|
-
...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"))
|
|
2637
2510
|
]);
|
|
2638
2511
|
const routes = exec.type === "lib" ? [] : await exec.getPageKeys();
|
|
2639
2512
|
const common = {
|
|
2640
2513
|
name: exec.name,
|
|
2641
2514
|
type: exec.type,
|
|
2642
2515
|
repoName: exec.workspace.repoName,
|
|
2643
|
-
serveDomain: WorkspaceExecutor.getBaseDevEnv(
|
|
2516
|
+
serveDomain: WorkspaceExecutor.getBaseDevEnv(path11.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
|
|
2644
2517
|
files,
|
|
2645
2518
|
libDeps,
|
|
2646
2519
|
pkgDeps,
|
|
@@ -2868,6 +2741,136 @@ class WorkspaceInfo {
|
|
|
2868
2741
|
}
|
|
2869
2742
|
}
|
|
2870
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
|
+
|
|
2871
2874
|
// pkgs/@akanjs/devkit/executors.ts
|
|
2872
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(" ")));
|
|
2873
2876
|
var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
|
|
@@ -3052,20 +3055,20 @@ class Executor {
|
|
|
3052
3055
|
const readPath = this.getPath(dirPath);
|
|
3053
3056
|
try {
|
|
3054
3057
|
const glob = new Bun.Glob("*");
|
|
3055
|
-
return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false }));
|
|
3058
|
+
return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false })).sort();
|
|
3056
3059
|
} catch {
|
|
3057
3060
|
return [];
|
|
3058
3061
|
}
|
|
3059
3062
|
}
|
|
3060
3063
|
async getAllFiles(pattern = "**/*", { cwd } = {}) {
|
|
3061
3064
|
const glob = new Bun.Glob(pattern);
|
|
3062
|
-
return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true }));
|
|
3065
|
+
return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true })).sort();
|
|
3063
3066
|
}
|
|
3064
3067
|
async getFilesAndDirs(dirPath) {
|
|
3065
3068
|
const fullDirPath = this.getPath(dirPath);
|
|
3066
3069
|
const glob = new Bun.Glob("*");
|
|
3067
|
-
const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
|
|
3068
|
-
const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
|
|
3070
|
+
const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true })).sort();
|
|
3071
|
+
const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false })).sort();
|
|
3069
3072
|
const dirs = allEntries.filter((entry) => !files.includes(entry));
|
|
3070
3073
|
return { files, dirs };
|
|
3071
3074
|
}
|
|
@@ -3370,6 +3373,7 @@ class WorkspaceExecutor extends Executor {
|
|
|
3370
3373
|
return new WorkspaceExecutor({ workspaceRoot, repoName });
|
|
3371
3374
|
}
|
|
3372
3375
|
static publicEnvPin = null;
|
|
3376
|
+
static localServeDomain = "localhost";
|
|
3373
3377
|
static getBaseDevEnv(envPath) {
|
|
3374
3378
|
const sourceEnv = {
|
|
3375
3379
|
...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
|
|
@@ -3378,12 +3382,9 @@ class WorkspaceExecutor extends Executor {
|
|
|
3378
3382
|
const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
|
|
3379
3383
|
const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
|
|
3380
3384
|
const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
|
|
3381
|
-
const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
|
|
3385
|
-
if (!serveDomain)
|
|
3386
|
-
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]);
|
|
3387
3388
|
const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
|
|
3388
3389
|
const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
|
|
3389
3390
|
if (!env)
|
|
@@ -3395,9 +3396,18 @@ class WorkspaceExecutor extends Executor {
|
|
|
3395
3396
|
serveDomain,
|
|
3396
3397
|
env,
|
|
3397
3398
|
portOffset,
|
|
3398
|
-
workspaceId
|
|
3399
|
+
workspaceId,
|
|
3400
|
+
defaulted
|
|
3399
3401
|
};
|
|
3400
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
|
+
}
|
|
3401
3411
|
getWorkspaceId({
|
|
3402
3412
|
allowEmpty
|
|
3403
3413
|
} = {}) {
|
|
@@ -3774,10 +3784,12 @@ class AppExecutor extends SysExecutor {
|
|
|
3774
3784
|
return basePort + appIndex + portOffset;
|
|
3775
3785
|
}
|
|
3776
3786
|
getCommandEnv(env = {}) {
|
|
3777
|
-
const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
|
|
3787
|
+
const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
|
|
3778
3788
|
const PORT = (8282 + portOffset).toString();
|
|
3779
3789
|
return {
|
|
3780
3790
|
...process.env,
|
|
3791
|
+
AKAN_PUBLIC_REPO_NAME: repoName,
|
|
3792
|
+
AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
|
|
3781
3793
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
3782
3794
|
AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
|
|
3783
3795
|
NODE_NO_WARNINGS: "1",
|
|
@@ -3928,7 +3940,7 @@ class AppExecutor extends SysExecutor {
|
|
|
3928
3940
|
pageKeys.push(key);
|
|
3929
3941
|
}
|
|
3930
3942
|
}
|
|
3931
|
-
pageKeys.sort();
|
|
3943
|
+
pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3932
3944
|
this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
|
|
3933
3945
|
return this.#pageKeys;
|
|
3934
3946
|
}
|
|
@@ -4174,7 +4186,7 @@ class PkgExecutor extends Executor {
|
|
|
4174
4186
|
}
|
|
4175
4187
|
async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
|
|
4176
4188
|
const dependencyNames = [...new Set(dependencies)].sort();
|
|
4177
|
-
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);
|
|
4178
4190
|
const dependencyVersions = new Map;
|
|
4179
4191
|
const missingDeps = [];
|
|
4180
4192
|
for (const dep of [...dependencyNames, ...devDependencyNames]) {
|