@akanjs/cli 3.1.0-rc.0 → 3.1.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/{CodeAgent-q7a4srt6.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-ak23dxc9.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-3v2d53gd.js → application.command-m1kcd7jf.js} +54 -42
- package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
- package/buildBatch.proc.js +24 -15
- package/{cloud.command-kk2rwkvw.js → cloud.command-9cp1t0f5.js} +63 -65
- package/cloud.script-dgstqt1m.js +24 -0
- package/{code.command-30sca09d.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-k7eqacq6.js → context.command-0qx447mb.js} +17 -13
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/agentRule/agentRule.instruction.md +3 -3
- package/guidelines/conventions/conventions.instruction.md +457 -1069
- package/guidelines/cssRule/cssRule.instruction.md +28 -7
- package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +48 -20
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +44 -11
- package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +135 -13
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
- package/incrementalBuilder.proc.js +10 -7
- package/index-192ha4xh.js +1173 -0
- package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
- package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
- package/{index-nz15k0b6.js → index-20bxxyvn.js} +80 -80
- package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
- package/{index-jyv387qw.js → index-4mhy6qme.js} +4 -4
- package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
- package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/index-665zp9rs.js +271 -0
- package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
- package/{index-2t4tqd1c.js → index-8fp3b47h.js} +419 -418
- package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
- package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-tn4rpqp1.js → index-bspm21y1.js} +73 -49
- package/index-cpb3nvma.js +667 -0
- package/{index-p9ystjrp.js → index-d47ypebv.js} +97 -55
- package/{index-0bm04n5g.js → index-d72hfmdm.js} +15 -13
- package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
- package/{index-y8q31011.js → index-fyq1z6kt.js} +68 -177
- package/{index-kjvrq3nc.js → index-g1mbppa9.js} +331 -250
- package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
- package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
- package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/index-mdjc93e1.js +33 -0
- package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
- package/{index-7bymavqk.js → index-qwa5k1j8.js} +1 -1
- package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
- package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
- package/index-vk0pa0mn.js +240 -0
- package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
- package/index.js +22 -22
- package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-9e84e32q.js → localRegistry.command-y176p558.js} +15 -11
- package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
- package/package.json +21 -21
- package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
- package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-f02j5tge.js → subspace.command-5c7yasd7.js} +424 -364
- package/templates/app/env/env.client.type.ts +2 -3
- package/templates/app/page/styles.css.template +8 -0
- 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-9x8nfn75.js → tunnel.command-2kw8vt9g.js} +8 -4
- package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-cmj6mqyk.js → workspace.command-6qfgq4b4.js} +38 -34
- package/TunnelShare-21mm6dr5.js +0 -9
- package/cloud.script-r7eyfey4.js +0 -20
- package/context.runner-hqd1ppqe.js +0 -21
- package/index-6w0rbr9d.js +0 -68
- package/index-n68jx79w.js +0 -120
- package/index-r2tpeb3n.js +0 -392
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// pkgs/@akanjs/devkit/frontendBuild/cssBlockParser.ts
|
|
3
|
+
class CssBlockParser {
|
|
4
|
+
#css;
|
|
5
|
+
#stack = [{ kind: "block", prelude: "", atRule: null, nodes: [] }];
|
|
6
|
+
#pendingParts = [];
|
|
7
|
+
#segmentStart = 0;
|
|
8
|
+
#parenDepth = 0;
|
|
9
|
+
constructor(css) {
|
|
10
|
+
this.#css = css;
|
|
11
|
+
}
|
|
12
|
+
static parse(css) {
|
|
13
|
+
return new CssBlockParser(css).#run();
|
|
14
|
+
}
|
|
15
|
+
static walk(root, context, visit) {
|
|
16
|
+
const stack = [{ nodes: root.nodes, idx: 0, context }];
|
|
17
|
+
while (stack.length > 0) {
|
|
18
|
+
const frame = stack[stack.length - 1];
|
|
19
|
+
const node = frame.nodes[frame.idx];
|
|
20
|
+
if (!node) {
|
|
21
|
+
stack.pop();
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
frame.idx += 1;
|
|
25
|
+
const childContext = visit(node, frame.context);
|
|
26
|
+
if (node.kind === "block" && childContext !== null)
|
|
27
|
+
stack.push({ nodes: node.nodes, idx: 0, context: childContext });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#run() {
|
|
31
|
+
const css = this.#css;
|
|
32
|
+
let idx = 0;
|
|
33
|
+
while (idx < css.length) {
|
|
34
|
+
const char = css[idx];
|
|
35
|
+
if (char === "/" && css[idx + 1] === "*") {
|
|
36
|
+
this.#pendingParts.push(css.slice(this.#segmentStart, idx));
|
|
37
|
+
const end = css.indexOf("*/", idx + 2);
|
|
38
|
+
idx = end === -1 ? css.length : end + 2;
|
|
39
|
+
this.#segmentStart = idx;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === "\\") {
|
|
43
|
+
idx += 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (char === '"' || char === "'") {
|
|
47
|
+
idx = this.#skipString(idx);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "(")
|
|
51
|
+
this.#parenDepth += 1;
|
|
52
|
+
else if (char === ")")
|
|
53
|
+
this.#parenDepth = Math.max(0, this.#parenDepth - 1);
|
|
54
|
+
else if (char === "{")
|
|
55
|
+
this.#openBlock(idx);
|
|
56
|
+
else if (char === "}")
|
|
57
|
+
this.#closeBlock(idx);
|
|
58
|
+
else if (char === ";" && this.#parenDepth === 0)
|
|
59
|
+
this.#endStatement(idx);
|
|
60
|
+
idx += 1;
|
|
61
|
+
}
|
|
62
|
+
this.#endStatement(css.length);
|
|
63
|
+
return this.#stack[0];
|
|
64
|
+
}
|
|
65
|
+
get #current() {
|
|
66
|
+
return this.#stack[this.#stack.length - 1];
|
|
67
|
+
}
|
|
68
|
+
#skipString(start) {
|
|
69
|
+
const css = this.#css;
|
|
70
|
+
const quote = css[start];
|
|
71
|
+
let idx = start + 1;
|
|
72
|
+
while (idx < css.length) {
|
|
73
|
+
const char = css[idx];
|
|
74
|
+
if (char === "\\")
|
|
75
|
+
idx += 2;
|
|
76
|
+
else if (char === quote)
|
|
77
|
+
return idx + 1;
|
|
78
|
+
else if (char === `
|
|
79
|
+
`)
|
|
80
|
+
return idx;
|
|
81
|
+
else
|
|
82
|
+
idx += 1;
|
|
83
|
+
}
|
|
84
|
+
return css.length;
|
|
85
|
+
}
|
|
86
|
+
#takeSegment(end) {
|
|
87
|
+
const text = this.#pendingParts.join("") + this.#css.slice(this.#segmentStart, end);
|
|
88
|
+
this.#pendingParts = [];
|
|
89
|
+
this.#segmentStart = end + 1;
|
|
90
|
+
return text.trim();
|
|
91
|
+
}
|
|
92
|
+
#openBlock(idx) {
|
|
93
|
+
const prelude = this.#takeSegment(idx);
|
|
94
|
+
const block = { kind: "block", prelude, atRule: CssBlockParser.#atRuleOf(prelude), nodes: [] };
|
|
95
|
+
this.#current.nodes.push(block);
|
|
96
|
+
this.#stack.push(block);
|
|
97
|
+
this.#parenDepth = 0;
|
|
98
|
+
}
|
|
99
|
+
#closeBlock(idx) {
|
|
100
|
+
this.#endStatement(idx);
|
|
101
|
+
if (this.#stack.length > 1)
|
|
102
|
+
this.#stack.pop();
|
|
103
|
+
this.#parenDepth = 0;
|
|
104
|
+
}
|
|
105
|
+
#endStatement(idx) {
|
|
106
|
+
const text = this.#takeSegment(idx);
|
|
107
|
+
if (!text || text.startsWith("@"))
|
|
108
|
+
return;
|
|
109
|
+
const colon = text.indexOf(":");
|
|
110
|
+
if (colon <= 0)
|
|
111
|
+
return;
|
|
112
|
+
const property = text.slice(0, colon).trim();
|
|
113
|
+
if (!/^[\w-]+$/.test(property))
|
|
114
|
+
return;
|
|
115
|
+
this.#current.nodes.push({ kind: "declaration", property, value: text.slice(colon + 1).trim() });
|
|
116
|
+
}
|
|
117
|
+
static #atRuleOf(prelude) {
|
|
118
|
+
return prelude.match(/^@[\w-]+/)?.[0].toLowerCase() ?? null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// pkgs/@akanjs/devkit/frontendBuild/styleContract.ts
|
|
123
|
+
var countBlocking = (violations) => violations.theme.length;
|
|
124
|
+
var formatStyleContract = (violations) => violations.theme.flatMap((theme) => [
|
|
125
|
+
` [error] contrast ${theme.scope} ${theme.pair} = ${theme.ratio}:1 (min ${theme.threshold}:1)`,
|
|
126
|
+
` \u2192 ${theme.suggestion}`
|
|
127
|
+
]).join(`
|
|
128
|
+
`);
|
|
129
|
+
|
|
130
|
+
// pkgs/@akanjs/devkit/frontendBuild/themeValidator.ts
|
|
131
|
+
var PAIRS = [
|
|
132
|
+
{ base: "background", fg: "foreground", threshold: 4.5 },
|
|
133
|
+
{ base: "primary", fg: "primary-foreground", threshold: 4.5 },
|
|
134
|
+
{ base: "secondary", fg: "secondary-foreground", threshold: 4.5 },
|
|
135
|
+
{ base: "accent", fg: "accent-foreground", threshold: 4.5 },
|
|
136
|
+
{ base: "neutral", fg: "neutral-foreground", threshold: 4.5 },
|
|
137
|
+
{ base: "card", fg: "card-foreground", threshold: 4.5 },
|
|
138
|
+
{ base: "popover", fg: "popover-foreground", threshold: 4.5 },
|
|
139
|
+
{ base: "muted", fg: "muted-foreground", threshold: 3 },
|
|
140
|
+
{ base: "info", fg: "info-foreground", threshold: 3 },
|
|
141
|
+
{ base: "success", fg: "success-foreground", threshold: 3 },
|
|
142
|
+
{ base: "warning", fg: "warning-foreground", threshold: 3 },
|
|
143
|
+
{ base: "destructive", fg: "destructive-foreground", threshold: 3 },
|
|
144
|
+
{ base: "open", fg: "open-foreground", threshold: 3 }
|
|
145
|
+
];
|
|
146
|
+
var THEME_SCOPES = new Set([":root", '[data-theme="dark"]', '[data-theme="light"]']);
|
|
147
|
+
var CONDITIONAL_AT_RULES = new Set(["@media", "@supports", "@container"]);
|
|
148
|
+
|
|
149
|
+
class ThemeValidator {
|
|
150
|
+
validate(css) {
|
|
151
|
+
const tokensByScope = ThemeValidator.parseThemeTokens(css);
|
|
152
|
+
const violations = [];
|
|
153
|
+
for (const [scope, tokens] of Object.entries(tokensByScope)) {
|
|
154
|
+
const underlying = THEME_SCOPES.has(scope) ? null : [...THEME_SCOPES].find((name) => scope.endsWith(` ${name}`));
|
|
155
|
+
const effective = underlying ? ThemeValidator.#overlayPairs(tokensByScope[underlying] ?? {}, tokens) : tokens;
|
|
156
|
+
violations.push(...this.validateScope(effective, scope));
|
|
157
|
+
}
|
|
158
|
+
return violations;
|
|
159
|
+
}
|
|
160
|
+
validateScope(tokens, scope) {
|
|
161
|
+
const violations = [];
|
|
162
|
+
for (const { base, fg, threshold } of PAIRS) {
|
|
163
|
+
const bg = tokens[base];
|
|
164
|
+
const front = tokens[fg];
|
|
165
|
+
if (!bg || !front)
|
|
166
|
+
continue;
|
|
167
|
+
const bgRgb = ThemeValidator.parseHex(bg);
|
|
168
|
+
const fgRgb = ThemeValidator.parseHex(front);
|
|
169
|
+
if (!bgRgb || !fgRgb)
|
|
170
|
+
continue;
|
|
171
|
+
const ratio = ThemeValidator.contrastRatio(bgRgb, fgRgb);
|
|
172
|
+
if (ratio >= threshold)
|
|
173
|
+
continue;
|
|
174
|
+
violations.push({
|
|
175
|
+
scope,
|
|
176
|
+
pair: `${base} / ${fg}`,
|
|
177
|
+
background: bg,
|
|
178
|
+
foreground: front,
|
|
179
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
180
|
+
threshold,
|
|
181
|
+
suggestion: `In ${scope}, --${base} (${bg}) against --${fg} (${front}) is ${ratio.toFixed(2)}:1, under the ${threshold}:1 minimum. Lighten or darken one of them until it clears.`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return violations;
|
|
185
|
+
}
|
|
186
|
+
static parseThemeTokens(css) {
|
|
187
|
+
const result = {};
|
|
188
|
+
CssBlockParser.walk(CssBlockParser.parse(css), "", (node, condition) => {
|
|
189
|
+
if (node.kind !== "block")
|
|
190
|
+
return null;
|
|
191
|
+
if (node.atRule === "@layer")
|
|
192
|
+
return condition;
|
|
193
|
+
if (node.atRule)
|
|
194
|
+
return CONDITIONAL_AT_RULES.has(node.atRule) ? node.prelude.replace(/\s+/g, " ") : null;
|
|
195
|
+
const scopes = node.prelude.split(",").map((selector) => ThemeValidator.#normalizeScope(selector)).filter((scope) => THEME_SCOPES.has(scope));
|
|
196
|
+
if (scopes.length === 0)
|
|
197
|
+
return null;
|
|
198
|
+
const tokens = ThemeValidator.#customProperties(node);
|
|
199
|
+
for (const scope of scopes) {
|
|
200
|
+
const key = condition ? `${condition} ${scope}` : scope;
|
|
201
|
+
result[key] = Object.assign(result[key] ?? {}, tokens);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
});
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
static #customProperties(block) {
|
|
208
|
+
const tokens = {};
|
|
209
|
+
for (const node of block.nodes) {
|
|
210
|
+
if (node.kind === "declaration" && node.property.startsWith("--"))
|
|
211
|
+
tokens[node.property.slice(2)] = node.value;
|
|
212
|
+
}
|
|
213
|
+
return tokens;
|
|
214
|
+
}
|
|
215
|
+
static #overlayPairs(underlying, tokens) {
|
|
216
|
+
const effective = {};
|
|
217
|
+
for (const name of PAIRS.flatMap(({ base, fg }) => [base, fg])) {
|
|
218
|
+
const value = tokens[name] ?? underlying[name];
|
|
219
|
+
if (value)
|
|
220
|
+
effective[name] = value;
|
|
221
|
+
}
|
|
222
|
+
return effective;
|
|
223
|
+
}
|
|
224
|
+
static #normalizeScope(selector) {
|
|
225
|
+
return selector.replace(/\[data-theme=['"]?([\w-]+)['"]?\]/g, '[data-theme="$1"]').trim();
|
|
226
|
+
}
|
|
227
|
+
static parseHex(value) {
|
|
228
|
+
const v = value.trim();
|
|
229
|
+
if (!v.startsWith("#"))
|
|
230
|
+
return null;
|
|
231
|
+
const hex = v.slice(1);
|
|
232
|
+
let full;
|
|
233
|
+
if (hex.length === 3 || hex.length === 4)
|
|
234
|
+
full = hex.slice(0, 3).split("").map((c) => c + c).join("");
|
|
235
|
+
else if (hex.length === 6 || hex.length === 8)
|
|
236
|
+
full = hex.slice(0, 6);
|
|
237
|
+
else
|
|
238
|
+
return null;
|
|
239
|
+
if (!/^[0-9a-fA-F]{6}$/.test(full))
|
|
240
|
+
return null;
|
|
241
|
+
const n = Number.parseInt(full, 16);
|
|
242
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
243
|
+
}
|
|
244
|
+
static #relativeLuminance([r, g, b]) {
|
|
245
|
+
const channel = (c) => {
|
|
246
|
+
const s = c / 255;
|
|
247
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
248
|
+
};
|
|
249
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
250
|
+
}
|
|
251
|
+
static contrastRatio(a, b) {
|
|
252
|
+
const la = ThemeValidator.#relativeLuminance(a);
|
|
253
|
+
const lb = ThemeValidator.#relativeLuminance(b);
|
|
254
|
+
const hi = Math.max(la, lb);
|
|
255
|
+
const lo = Math.min(la, lb);
|
|
256
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { countBlocking, formatStyleContract, CssBlockParser, ThemeValidator };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
resolveSsrPageEntriesForApp
|
|
4
|
+
} from "./index-va06nr10.js";
|
|
5
|
+
import {
|
|
6
|
+
bundleDefine,
|
|
7
|
+
CsrEntryFiles,
|
|
8
|
+
PagesEntrySourceGenerator,
|
|
9
|
+
PagesBundleBuilder
|
|
10
|
+
} from "./index-192ha4xh.js";
|
|
11
|
+
|
|
12
|
+
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
13
|
+
import { mkdir, rm, unlink } from "fs/promises";
|
|
14
|
+
import path2 from "path";
|
|
15
|
+
|
|
16
|
+
// pkgs/@akanjs/devkit/transforms/browserDirnamePlugin.ts
|
|
17
|
+
import path from "path";
|
|
18
|
+
var vendorScript = /[\\/]node_modules[\\/].+\.[cm]?js$/;
|
|
19
|
+
var createBrowserDirnamePlugin = () => ({
|
|
20
|
+
name: "akan-browser-dirname",
|
|
21
|
+
setup(build) {
|
|
22
|
+
build.onLoad({ filter: vendorScript }, async (args) => {
|
|
23
|
+
const source = await Bun.file(args.path).text();
|
|
24
|
+
if (!source.includes("__dirname") && !source.includes("__filename"))
|
|
25
|
+
return;
|
|
26
|
+
const posixPath = args.path.replaceAll("\\", "/");
|
|
27
|
+
const filename = posixPath.slice(posixPath.indexOf("/node_modules/"));
|
|
28
|
+
const define = { __dirname: JSON.stringify(path.posix.dirname(filename)), __filename: JSON.stringify(filename) };
|
|
29
|
+
return {
|
|
30
|
+
loader: "js",
|
|
31
|
+
contents: new Bun.Transpiler({ loader: "js", target: "browser", define }).transformSync(source)
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
38
|
+
class CsrArtifactBuilder {
|
|
39
|
+
#app;
|
|
40
|
+
#command;
|
|
41
|
+
#lang;
|
|
42
|
+
#cssAssets;
|
|
43
|
+
constructor(app, command = "start", lang = "en", { cssAssets = null } = {}) {
|
|
44
|
+
this.#app = app;
|
|
45
|
+
this.#command = command;
|
|
46
|
+
this.#lang = lang;
|
|
47
|
+
this.#cssAssets = cssAssets;
|
|
48
|
+
}
|
|
49
|
+
async build() {
|
|
50
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
51
|
+
if (pageKeys.length === 0) {
|
|
52
|
+
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page \u2014 skipping CSR build`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
|
|
56
|
+
const akanConfig = await this.#app.getConfig();
|
|
57
|
+
const cssAssets = await this.#loadCssAssets();
|
|
58
|
+
const basePaths = [...akanConfig.basePaths];
|
|
59
|
+
const htmlBasePaths = basePaths.length > 0 ? basePaths : [""];
|
|
60
|
+
await rm(this.#outputDir, { recursive: true, force: true });
|
|
61
|
+
await mkdir(this.#generatedDir, { recursive: true });
|
|
62
|
+
const generatedFiles = Object.fromEntries((await Promise.all(htmlBasePaths.map(async (basePath) => [
|
|
63
|
+
this.#createHtmlFile(basePath),
|
|
64
|
+
await this.#createEntryFile(basePath, CsrArtifactBuilder.pageEntriesForBasePath(pageEntries, basePath, basePaths))
|
|
65
|
+
]))).flat());
|
|
66
|
+
const options = CsrArtifactBuilder.bundleOptions(this.#app, this.#command, akanConfig.optimizeImports);
|
|
67
|
+
const result = await Bun.build({
|
|
68
|
+
...options,
|
|
69
|
+
entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
|
|
70
|
+
files: generatedFiles,
|
|
71
|
+
root: this.#generatedDir,
|
|
72
|
+
outdir: this.#outputDir
|
|
73
|
+
});
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
const logs = result.logs.map((log) => log.message).join(`
|
|
76
|
+
`);
|
|
77
|
+
throw new Error(`[csr-build] failed${logs ? `
|
|
78
|
+
${logs}` : ""}`);
|
|
79
|
+
}
|
|
80
|
+
await this.#inlineCsrArtifacts(cssAssets);
|
|
81
|
+
if (this.#command === "build")
|
|
82
|
+
await CsrArtifactBuilder.writePublicEnv(this.#app, options.define);
|
|
83
|
+
this.#app.verbose(`[csr-build] output -> ${this.#outputDir}`);
|
|
84
|
+
return { outputDir: this.#outputDir };
|
|
85
|
+
}
|
|
86
|
+
static bundleOptions(app, command, optimizeImports = []) {
|
|
87
|
+
return {
|
|
88
|
+
target: "browser",
|
|
89
|
+
splitting: false,
|
|
90
|
+
minify: true,
|
|
91
|
+
define: bundleDefine(app, command, "csr"),
|
|
92
|
+
optimizeImports,
|
|
93
|
+
plugins: [PagesBundleBuilder.createCssStubPlugin(), createBrowserDirnamePlugin()]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
static publicEnvPath(app) {
|
|
97
|
+
return path2.join(app.dist.cwdPath, ".akan", "artifact", "csr-public-env.json");
|
|
98
|
+
}
|
|
99
|
+
static async writePublicEnv(app, define) {
|
|
100
|
+
const prefix = "process.env.";
|
|
101
|
+
const publicEnv = Object.fromEntries(Object.entries(define).filter(([key]) => key.startsWith(`${prefix}AKAN_PUBLIC_`)).map(([key, value]) => [key.slice(prefix.length), JSON.parse(value)]));
|
|
102
|
+
await Bun.write(CsrArtifactBuilder.publicEnvPath(app), `${JSON.stringify(publicEnv, null, 2)}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
static pageEntriesForBasePath(pageEntries, basePath, basePaths) {
|
|
106
|
+
return CsrEntryFiles.pageEntriesForBasePath(pageEntries, basePath, basePaths);
|
|
107
|
+
}
|
|
108
|
+
static htmlFilename(basePath) {
|
|
109
|
+
return `${basePath || "index"}.html`;
|
|
110
|
+
}
|
|
111
|
+
static entryFilename(basePath) {
|
|
112
|
+
return CsrEntryFiles.entryFilename(basePath);
|
|
113
|
+
}
|
|
114
|
+
static basePathOfHtml(htmlPath) {
|
|
115
|
+
const name = path2.basename(htmlPath, ".html");
|
|
116
|
+
return name === "index" ? "" : name;
|
|
117
|
+
}
|
|
118
|
+
get #outputDir() {
|
|
119
|
+
return path2.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, this.#command === "build" ? "csr" : ".akan/artifact/csr");
|
|
120
|
+
}
|
|
121
|
+
get #generatedDir() {
|
|
122
|
+
return path2.join(this.#app.cwdPath, ".akan/generated/csr");
|
|
123
|
+
}
|
|
124
|
+
get #artifactDir() {
|
|
125
|
+
return path2.join(this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath, ".akan/artifact");
|
|
126
|
+
}
|
|
127
|
+
#generatedPath(filename) {
|
|
128
|
+
return path2.join(this.#generatedDir, filename);
|
|
129
|
+
}
|
|
130
|
+
async#createEntryFile(basePath, pageEntries) {
|
|
131
|
+
return [
|
|
132
|
+
this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
|
|
133
|
+
`import { bootCsr } from "akanjs/webkit";
|
|
134
|
+
${await PagesEntrySourceGenerator.generateStatic(pageEntries)}
|
|
135
|
+
void bootCsr(pages);
|
|
136
|
+
`
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
#createHtmlFile(basePath) {
|
|
140
|
+
return [
|
|
141
|
+
this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath)),
|
|
142
|
+
`<!doctype html>
|
|
143
|
+
<html lang="${this.#lang}">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="utf-8" />
|
|
146
|
+
<title>${this.#app.name}</title>
|
|
147
|
+
<base href="/" />
|
|
148
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<div id="root"></div>
|
|
152
|
+
<script type="module" src="./${CsrArtifactBuilder.entryFilename(basePath)}"></script>
|
|
153
|
+
</body>
|
|
154
|
+
</html>
|
|
155
|
+
`
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
async#loadCssAssets() {
|
|
159
|
+
if (this.#cssAssets)
|
|
160
|
+
return this.#cssAssets;
|
|
161
|
+
const artifactFile = Bun.file(path2.join(this.#artifactDir, "base-artifact.json"));
|
|
162
|
+
if (!await artifactFile.exists())
|
|
163
|
+
return {};
|
|
164
|
+
const artifact = await artifactFile.json();
|
|
165
|
+
return artifact.cssAssets ?? {};
|
|
166
|
+
}
|
|
167
|
+
async#inlineCsrArtifacts(cssAssets) {
|
|
168
|
+
const jsFiles = new Set;
|
|
169
|
+
for (const htmlPath of await this.#listOutputFiles((filePath) => filePath.endsWith(".html"))) {
|
|
170
|
+
const htmlFile = Bun.file(htmlPath);
|
|
171
|
+
if (!await htmlFile.exists())
|
|
172
|
+
continue;
|
|
173
|
+
const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
|
|
174
|
+
const cssAsset = cssAssets[basePath];
|
|
175
|
+
if (!cssAsset) {
|
|
176
|
+
this.#app.logger.warn(`[csr-build] base-artifact.json has no compiled stylesheet for ${basePath || "root"}; ${path2.basename(htmlPath)} ships without CSS`);
|
|
177
|
+
}
|
|
178
|
+
const inlined = await this.#inlineHtmlAssets(await htmlFile.text(), htmlPath, cssAsset);
|
|
179
|
+
for (const filePath of inlined.jsFiles)
|
|
180
|
+
jsFiles.add(filePath);
|
|
181
|
+
await Bun.write(htmlPath, inlined.html);
|
|
182
|
+
}
|
|
183
|
+
for (const filePath of jsFiles)
|
|
184
|
+
await unlink(filePath).catch(() => {
|
|
185
|
+
return;
|
|
186
|
+
});
|
|
187
|
+
const remainingAssets = await this.#listOutputFiles((filePath) => filePath.endsWith(".js") || filePath.endsWith(".css"));
|
|
188
|
+
if (remainingAssets.length > 0) {
|
|
189
|
+
throw new Error(`[csr-build] expected single-file HTML, but CSR assets remain:
|
|
190
|
+
${remainingAssets.join(`
|
|
191
|
+
`)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async#inlineHtmlAssets(html, htmlPath, cssAsset) {
|
|
195
|
+
let next = html;
|
|
196
|
+
if (cssAsset) {
|
|
197
|
+
const css = await Bun.file(path2.join(this.#artifactDir, cssAsset.cssRelPath)).text();
|
|
198
|
+
next = CsrArtifactBuilder.injectBeforeHeadEnd(next, CsrArtifactBuilder.createInlineStyle(css));
|
|
199
|
+
}
|
|
200
|
+
const jsFiles = [];
|
|
201
|
+
next = await CsrArtifactBuilder.replaceModuleScriptSrc(next, async (src) => {
|
|
202
|
+
const jsPath = CsrArtifactBuilder.resolveHtmlAssetPath(htmlPath, src);
|
|
203
|
+
jsFiles.push(jsPath);
|
|
204
|
+
return await Bun.file(jsPath).text();
|
|
205
|
+
});
|
|
206
|
+
return { html: next, jsFiles };
|
|
207
|
+
}
|
|
208
|
+
async#listOutputFiles(predicate) {
|
|
209
|
+
const glob = new Bun.Glob("**/*");
|
|
210
|
+
const files = [];
|
|
211
|
+
for await (const filePath of glob.scan({ cwd: this.#outputDir, absolute: true })) {
|
|
212
|
+
if (predicate(filePath))
|
|
213
|
+
files.push(filePath);
|
|
214
|
+
}
|
|
215
|
+
return files.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
216
|
+
}
|
|
217
|
+
static injectBeforeHeadEnd(html, snippet) {
|
|
218
|
+
const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
|
|
219
|
+
const headEnd = scannable.search(/<\/head\s*>/i);
|
|
220
|
+
if (headEnd !== -1)
|
|
221
|
+
return `${html.slice(0, headEnd)}${snippet}
|
|
222
|
+
${html.slice(headEnd)}`;
|
|
223
|
+
const bodyStart = scannable.search(/<body(?:\s|>)/i);
|
|
224
|
+
if (bodyStart !== -1)
|
|
225
|
+
return `${html.slice(0, bodyStart)}${snippet}
|
|
226
|
+
${html.slice(bodyStart)}`;
|
|
227
|
+
return `${html}
|
|
228
|
+
${snippet}`;
|
|
229
|
+
}
|
|
230
|
+
static blankEmbeddedContent(html) {
|
|
231
|
+
return html.replace(/<script\b[^>]*>[\s\S]*?(?:<\/script(?:[\s/][^<>]*)?>|$)|<style\b[^>]*>[\s\S]*?(?:<\/style(?:[\s/][^<>]*)?>|$)|<!--[\s\S]*?(?:--!?>|$)/gi, (match) => " ".repeat(match.length));
|
|
232
|
+
}
|
|
233
|
+
static createInlineStyle(css) {
|
|
234
|
+
return `<style data-akan-css="active">
|
|
235
|
+
${css.replace(/<\/style/gi, "<\\/style")}
|
|
236
|
+
</style>`;
|
|
237
|
+
}
|
|
238
|
+
static async replaceModuleScriptSrc(html, loadScript) {
|
|
239
|
+
const scriptRe = /<script\b(?=[^>]*\btype=["']module["'])(?=[^>]*\bsrc=["']([^"']+)["'])[^>]*>\s*<\/script>/gi;
|
|
240
|
+
let result = "";
|
|
241
|
+
let lastIndex = 0;
|
|
242
|
+
let matched = false;
|
|
243
|
+
for (const match of html.matchAll(scriptRe)) {
|
|
244
|
+
const full = match[0];
|
|
245
|
+
const src = match[1];
|
|
246
|
+
if (match.index === undefined || !src)
|
|
247
|
+
continue;
|
|
248
|
+
matched = true;
|
|
249
|
+
result += html.slice(lastIndex, match.index);
|
|
250
|
+
result += `<script type="module">
|
|
251
|
+
${CsrArtifactBuilder.escapeInlineScript(await loadScript(src))}
|
|
252
|
+
</script>`;
|
|
253
|
+
lastIndex = match.index + full.length;
|
|
254
|
+
}
|
|
255
|
+
if (!matched)
|
|
256
|
+
return html;
|
|
257
|
+
return result + html.slice(lastIndex);
|
|
258
|
+
}
|
|
259
|
+
static escapeInlineScript(source) {
|
|
260
|
+
return source.replace(/<\/script/gi, "<\\/script");
|
|
261
|
+
}
|
|
262
|
+
static resolveHtmlAssetPath(htmlPath, src) {
|
|
263
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//")) {
|
|
264
|
+
throw new Error(`[csr-build] cannot inline external script: ${src}`);
|
|
265
|
+
}
|
|
266
|
+
const normalized = src.startsWith("/") ? src.slice(1) : src;
|
|
267
|
+
return path2.resolve(path2.dirname(htmlPath), normalized);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { CsrArtifactBuilder };
|