@akanjs/devkit 1.0.20 → 2.1.0-rc.1
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/README.ko.md +65 -0
- package/README.md +62 -6
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +351 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BunPlugin } from "bun";
|
|
4
|
+
import type {
|
|
5
|
+
ApplicationBuildPhaseResult,
|
|
6
|
+
ApplicationBuildProgressReporter,
|
|
7
|
+
ApplicationBuildResult,
|
|
8
|
+
} from "./applicationBuildReporter";
|
|
9
|
+
import type { App } from "./commandDecorators";
|
|
10
|
+
import { AllRoutesBuilder, CsrArtifactBuilder, precompressArtifacts, SsrBaseArtifactBuilder } from "./frontendBuild";
|
|
11
|
+
import { Spinner } from "./spinner";
|
|
12
|
+
|
|
13
|
+
export interface TypecheckOptions {
|
|
14
|
+
clean?: boolean;
|
|
15
|
+
incremental?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type BuildPhaseId = "prepare" | "typecheck" | "backend" | "ssr" | "csr" | "compress" | "metadata";
|
|
19
|
+
|
|
20
|
+
export type BuildPhaseResult = ApplicationBuildPhaseResult & { id: BuildPhaseId };
|
|
21
|
+
export type BuildResult = ApplicationBuildResult;
|
|
22
|
+
export type BuildProgressReporter = ApplicationBuildProgressReporter;
|
|
23
|
+
export interface ApplicationBuildRunnerOptions {
|
|
24
|
+
fast?: boolean;
|
|
25
|
+
reporter?: BuildProgressReporter;
|
|
26
|
+
}
|
|
27
|
+
export interface BuildOptions {
|
|
28
|
+
spinner?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface BuildPhaseRunOptions {
|
|
31
|
+
spinner?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BUILD_PHASE_EMOJIS: Record<BuildPhaseId, string> = {
|
|
35
|
+
prepare: "🧹",
|
|
36
|
+
typecheck: "🔎",
|
|
37
|
+
backend: "📦",
|
|
38
|
+
ssr: "🧭",
|
|
39
|
+
csr: "🎨",
|
|
40
|
+
compress: "🗜️",
|
|
41
|
+
metadata: "📝",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SSR_RENDER_EXTERNALS = [
|
|
45
|
+
"react",
|
|
46
|
+
"react/jsx-runtime",
|
|
47
|
+
"react/jsx-dev-runtime",
|
|
48
|
+
"react-dom",
|
|
49
|
+
"react-dom/server.browser",
|
|
50
|
+
"react-server-dom-webpack/client.node",
|
|
51
|
+
"react-server-dom-webpack/client.browser",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
const TYPECHECK_WORKER_CODE = `
|
|
55
|
+
import { TypeChecker } from "@akanjs/devkit";
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
|
|
59
|
+
if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
|
|
60
|
+
const result = TypeChecker.checkProject(configPath);
|
|
61
|
+
if (result.errors.length > 0) {
|
|
62
|
+
console.error(result.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export class ApplicationBuildRunner {
|
|
72
|
+
#app: App;
|
|
73
|
+
#fast: boolean;
|
|
74
|
+
#reporter?: BuildProgressReporter;
|
|
75
|
+
#startedAt = Date.now();
|
|
76
|
+
#phases: BuildPhaseResult[] = [];
|
|
77
|
+
|
|
78
|
+
constructor(app: App, { fast = false, reporter }: ApplicationBuildRunnerOptions = {}) {
|
|
79
|
+
this.#app = app;
|
|
80
|
+
this.#fast = fast;
|
|
81
|
+
this.#reporter = reporter;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
|
|
85
|
+
// serial build is needed because of Bun.build is unstable for parallel build
|
|
86
|
+
const phaseOptions = { spinner };
|
|
87
|
+
await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
|
|
88
|
+
spinner,
|
|
89
|
+
});
|
|
90
|
+
if (!this.#fast) await this.#runPhase("typecheck", "Typechecking", () => this.typecheck(), undefined, phaseOptions);
|
|
91
|
+
await this.#runPhase(
|
|
92
|
+
"backend",
|
|
93
|
+
"Compiling backend",
|
|
94
|
+
() => this.#buildBackend(),
|
|
95
|
+
(result) => `${result.entrypoints} entrypoints, ${result.outputs} outputs`,
|
|
96
|
+
phaseOptions,
|
|
97
|
+
);
|
|
98
|
+
await this.#runPhase(
|
|
99
|
+
"ssr",
|
|
100
|
+
"Building SSR route artifacts",
|
|
101
|
+
() => this.#buildSsr(),
|
|
102
|
+
(result) =>
|
|
103
|
+
result
|
|
104
|
+
? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries`
|
|
105
|
+
: "skipped",
|
|
106
|
+
phaseOptions,
|
|
107
|
+
);
|
|
108
|
+
await this.#runPhase(
|
|
109
|
+
"csr",
|
|
110
|
+
"Building CSR assets",
|
|
111
|
+
() => this.#buildCsr(),
|
|
112
|
+
(result) => result?.outputDir ?? "skipped",
|
|
113
|
+
phaseOptions,
|
|
114
|
+
);
|
|
115
|
+
await this.#runPhase(
|
|
116
|
+
"compress",
|
|
117
|
+
"Compressing static assets",
|
|
118
|
+
() => precompressArtifacts(this.#app),
|
|
119
|
+
(result) =>
|
|
120
|
+
result.files > 0
|
|
121
|
+
? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> ${ApplicationBuildRunner.formatBytes(result.outputBytes)}`
|
|
122
|
+
: "no files",
|
|
123
|
+
phaseOptions,
|
|
124
|
+
);
|
|
125
|
+
await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta(), undefined, {
|
|
126
|
+
spinner,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
phases: this.#phases,
|
|
130
|
+
durationMs: Date.now() - this.#startedAt,
|
|
131
|
+
outputDir: this.#app.dist.cwdPath,
|
|
132
|
+
artifactDir: path.join(this.#app.dist.cwdPath, ".akan/artifact"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async typecheck(options: TypecheckOptions = {}) {
|
|
137
|
+
const { clean = false, incremental = true } = options;
|
|
138
|
+
await this.#app.getPageKeys({ refresh: true });
|
|
139
|
+
const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
|
|
140
|
+
if (clean) await rm(path.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
|
|
141
|
+
await this.#checkProjectInChildProcess(tsconfigPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async #runPhase<T>(
|
|
145
|
+
id: BuildPhaseId,
|
|
146
|
+
label: string,
|
|
147
|
+
task: () => Promise<T>,
|
|
148
|
+
summarize?: (result: T) => string | undefined,
|
|
149
|
+
options: BuildPhaseRunOptions = {},
|
|
150
|
+
) {
|
|
151
|
+
this.#reporter?.phaseStart?.({ id, label });
|
|
152
|
+
const phaseStartedAt = Date.now();
|
|
153
|
+
const spinner = options.spinner
|
|
154
|
+
? new Spinner(label, { prefix: `${BUILD_PHASE_EMOJIS[id]} ${id}` }).start()
|
|
155
|
+
: undefined;
|
|
156
|
+
try {
|
|
157
|
+
const result = await task();
|
|
158
|
+
const phase = { id, label, durationMs: Date.now() - phaseStartedAt, summary: summarize?.(result) };
|
|
159
|
+
this.#phases.push(phase);
|
|
160
|
+
const summary = phase.summary ? `: ${phase.summary}` : "";
|
|
161
|
+
spinner?.succeed(`${label}${summary}`);
|
|
162
|
+
this.#reporter?.phaseDone?.(phase);
|
|
163
|
+
return result;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
spinner?.fail(`${label} failed`);
|
|
166
|
+
this.#reporter?.phaseFail?.({ id, label }, error);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async #buildAppMeta() {
|
|
172
|
+
const akanConfig = await this.#app.getConfig();
|
|
173
|
+
await Promise.all([
|
|
174
|
+
this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
|
|
175
|
+
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.docker.content),
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async #buildBackend() {
|
|
180
|
+
const akanConfig = await this.#app.getConfig();
|
|
181
|
+
const backendExternals = [...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS])];
|
|
182
|
+
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
183
|
+
for (const entrypoint of backendEntryPoints) {
|
|
184
|
+
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
185
|
+
}
|
|
186
|
+
const backendResult = await this.#buildOrThrow("backend", {
|
|
187
|
+
entrypoints: backendEntryPoints,
|
|
188
|
+
outdir: this.#app.dist.cwdPath,
|
|
189
|
+
target: "bun",
|
|
190
|
+
minify: true,
|
|
191
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
192
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
193
|
+
});
|
|
194
|
+
const rscWorkerResult = await this.#buildOrThrow("rsc-worker", {
|
|
195
|
+
entrypoints: [this.#resolveRscWorkerBuildEntry()],
|
|
196
|
+
outdir: this.#app.dist.cwdPath,
|
|
197
|
+
target: "bun",
|
|
198
|
+
minify: true,
|
|
199
|
+
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
200
|
+
conditions: ["react-server"],
|
|
201
|
+
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
202
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
203
|
+
plugins:
|
|
204
|
+
akanConfig.externalLibs.length > 0 ? [this.#createExternalSpecifiersPlugin(akanConfig.externalLibs)] : [],
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
entrypoints: backendEntryPoints.length + 1,
|
|
208
|
+
outputs: backendResult.outputs.length + rscWorkerResult.outputs.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#resolveRscWorkerBuildEntry(): string {
|
|
213
|
+
try {
|
|
214
|
+
return Bun.resolveSync("akanjs/server/rsc-worker", import.meta.dir);
|
|
215
|
+
} catch {
|
|
216
|
+
return path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server/rscWorker.tsx");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async #buildCsr() {
|
|
221
|
+
return await new CsrArtifactBuilder(this.#app, "build").build();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async #buildSsr() {
|
|
225
|
+
const pageKeys = await this.#app.getPageKeys();
|
|
226
|
+
if (pageKeys.length === 0) {
|
|
227
|
+
this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page — skipping SSR build`);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const base = await new SsrBaseArtifactBuilder(this.#app, "build").build();
|
|
231
|
+
const allRoutes = await new AllRoutesBuilder(this.#app, base.artifact, "build").build();
|
|
232
|
+
return { base, allRoutes };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async #writeTypecheckTsconfig({ incremental = true }: TypecheckOptions = {}) {
|
|
236
|
+
const typecheckDir = path.join(this.#app.cwdPath, ".akan", "typecheck");
|
|
237
|
+
await mkdir(typecheckDir, { recursive: true });
|
|
238
|
+
const tsconfig = {
|
|
239
|
+
extends: "../../tsconfig.json",
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
noEmit: true,
|
|
242
|
+
incremental,
|
|
243
|
+
tsBuildInfoFile: "./tsconfig.tsbuildinfo",
|
|
244
|
+
},
|
|
245
|
+
include: [
|
|
246
|
+
"../../main.ts",
|
|
247
|
+
"../../server.ts",
|
|
248
|
+
"../../client.ts",
|
|
249
|
+
"../../page/**/*.ts",
|
|
250
|
+
"../../page/**/*.tsx",
|
|
251
|
+
"../../../../pkgs/akanjs/*/types/**/*.d.ts",
|
|
252
|
+
],
|
|
253
|
+
references: [],
|
|
254
|
+
};
|
|
255
|
+
const tsconfigPath = path.join(typecheckDir, "tsconfig.json");
|
|
256
|
+
await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
|
|
257
|
+
return { typecheckDir, tsconfigPath };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async #checkProjectInChildProcess(tsconfigPath: string) {
|
|
261
|
+
const proc = Bun.spawn([process.execPath, "--eval", TYPECHECK_WORKER_CODE], {
|
|
262
|
+
cwd: this.#app.workspace.workspaceRoot,
|
|
263
|
+
env: this.#app.getCommandEnv({
|
|
264
|
+
AKAN_COMMAND_TYPE: "typecheck",
|
|
265
|
+
AKAN_TYPECHECK_TSCONFIG: tsconfigPath,
|
|
266
|
+
}),
|
|
267
|
+
stdout: "pipe",
|
|
268
|
+
stderr: "pipe",
|
|
269
|
+
});
|
|
270
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
271
|
+
new Response(proc.stdout).text(),
|
|
272
|
+
new Response(proc.stderr).text(),
|
|
273
|
+
proc.exited,
|
|
274
|
+
]);
|
|
275
|
+
if (exitCode !== 0) throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async #buildOrThrow(label: string, config: Bun.BuildConfig): Promise<Bun.BuildOutput> {
|
|
279
|
+
const result = await Bun.build(config);
|
|
280
|
+
if (!result.success) throw new AggregateError(result.logs, `[${label}] Bun.build failed`);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#createExternalSpecifiersPlugin(specifiers: readonly string[]): BunPlugin {
|
|
285
|
+
const uniqueSpecifiers = [...new Set(specifiers)];
|
|
286
|
+
const escaped = uniqueSpecifiers.map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
287
|
+
const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
name: "akan-backend-externalize-specifiers",
|
|
291
|
+
setup(build) {
|
|
292
|
+
build.onResolve({ filter }, (args) => ({ path: args.path, external: true }));
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static formatBytes(bytes: number): string {
|
|
298
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
299
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
300
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import type { App } from "./commandDecorators";
|
|
3
|
+
import { FileSys } from "./fileSys";
|
|
4
|
+
import { uploadRelease } from "./uploadRelease";
|
|
5
|
+
|
|
6
|
+
export interface ReleaseSourceOptions {
|
|
7
|
+
rebuild?: boolean;
|
|
8
|
+
buildNum?: number;
|
|
9
|
+
environment?: string;
|
|
10
|
+
local?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ApplicationReleasePackagerOptions {
|
|
14
|
+
build: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ApplicationReleasePackager {
|
|
18
|
+
#app: App;
|
|
19
|
+
#build: () => Promise<void>;
|
|
20
|
+
|
|
21
|
+
constructor(app: App, options: ApplicationReleasePackagerOptions) {
|
|
22
|
+
this.#app = app;
|
|
23
|
+
this.#build = options.build;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async releaseSource({
|
|
27
|
+
rebuild,
|
|
28
|
+
buildNum = 0,
|
|
29
|
+
environment = "debug",
|
|
30
|
+
local = true,
|
|
31
|
+
}: ReleaseSourceOptions = {}): Promise<void> {
|
|
32
|
+
const { platformVersion } = await this.#prepareReleaseBuild({ rebuild, buildNum });
|
|
33
|
+
await this.#writeSourceArchive({ readme: this.#confidentialReadme() });
|
|
34
|
+
await uploadRelease(this.#app.name, {
|
|
35
|
+
local,
|
|
36
|
+
buildNum,
|
|
37
|
+
environment,
|
|
38
|
+
platformVersion,
|
|
39
|
+
workspaceRoot: this.#app.workspace.cwdPath,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async compressProjectFiles({ rebuild, buildNum = 0 }: ReleaseSourceOptions = {}): Promise<void> {
|
|
44
|
+
await this.#prepareReleaseBuild({ rebuild, buildNum });
|
|
45
|
+
await this.#writeSourceArchive({ readme: this.#publicReadme() });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async #prepareReleaseBuild({ rebuild, buildNum }: { rebuild?: boolean; buildNum: number }) {
|
|
49
|
+
const akanConfig = await this.#app.getConfig();
|
|
50
|
+
const platformVersion = akanConfig.mobile.version;
|
|
51
|
+
const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
|
|
52
|
+
if (await FileSys.dirExists(buildRoot)) await rm(buildRoot, { recursive: true, force: true });
|
|
53
|
+
await mkdir(buildRoot, { recursive: true });
|
|
54
|
+
if (rebuild || !(await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))) await this.#build();
|
|
55
|
+
|
|
56
|
+
const buildVersion = `${platformVersion}-${buildNum}`;
|
|
57
|
+
const buildPath = `${buildRoot}/${buildVersion}`;
|
|
58
|
+
await mkdir(buildPath, { recursive: true });
|
|
59
|
+
await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
|
|
60
|
+
|
|
61
|
+
await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
|
|
62
|
+
await rm(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
|
|
63
|
+
|
|
64
|
+
await this.#app.workspace.spawn("tar", [
|
|
65
|
+
"-zcf",
|
|
66
|
+
`${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-release.tar.gz`,
|
|
67
|
+
"-C",
|
|
68
|
+
buildRoot,
|
|
69
|
+
"./",
|
|
70
|
+
]);
|
|
71
|
+
await this.#writeCsrZipIfPresent();
|
|
72
|
+
return { platformVersion };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async #writeCsrZipIfPresent(): Promise<void> {
|
|
76
|
+
if (!(await FileSys.dirExists(`${this.#app.dist.cwdPath}/csr`))) return;
|
|
77
|
+
await cp(`${this.#app.dist.cwdPath}/csr`, "./csr", { recursive: true });
|
|
78
|
+
await this.#app.workspace.spawn("zip", [
|
|
79
|
+
"-r",
|
|
80
|
+
`${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
|
|
81
|
+
"./csr",
|
|
82
|
+
]);
|
|
83
|
+
await rm("./csr", { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async #writeSourceArchive({ readme }: { readme: string }): Promise<void> {
|
|
87
|
+
const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
|
|
88
|
+
await this.#resetSourceRoot(sourceRoot);
|
|
89
|
+
await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
|
|
90
|
+
|
|
91
|
+
const libDeps = ["social", "shared", "platform", "util"];
|
|
92
|
+
await Promise.all(
|
|
93
|
+
libDeps.map((lib) =>
|
|
94
|
+
cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true }),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
await Promise.all(
|
|
98
|
+
[".next", "ios", "android", "public/libs"].map(async (path) => {
|
|
99
|
+
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path}`;
|
|
100
|
+
if (await FileSys.dirExists(targetPath)) await rm(targetPath, { recursive: true, force: true });
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const syncPaths = [".husky", ".gitignore", "package.json"];
|
|
105
|
+
await Promise.all(
|
|
106
|
+
syncPaths.map((path) =>
|
|
107
|
+
cp(`${this.#app.workspace.cwdPath}/${path}`, `${sourceRoot}/${path}`, { recursive: true }),
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
await this.#writeSourceTsconfig(sourceRoot, libDeps);
|
|
111
|
+
await Bun.write(`${sourceRoot}/README.md`, readme);
|
|
112
|
+
await this.#app.workspace.spawn("tar", [
|
|
113
|
+
"-zcf",
|
|
114
|
+
`${this.#app.workspace.cwdPath}/releases/sources/${this.#app.name}-source.tar.gz`,
|
|
115
|
+
"-C",
|
|
116
|
+
sourceRoot,
|
|
117
|
+
"./",
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async #resetSourceRoot(sourceRoot: string): Promise<void> {
|
|
122
|
+
if (await FileSys.dirExists(sourceRoot)) {
|
|
123
|
+
const maxRetry = 3;
|
|
124
|
+
for (let i = 0; i < maxRetry; i++) {
|
|
125
|
+
try {
|
|
126
|
+
await rm(sourceRoot, { recursive: true, force: true });
|
|
127
|
+
} catch {
|
|
128
|
+
//
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
await mkdir(sourceRoot, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async #writeSourceTsconfig(sourceRoot: string, libDeps: string[]): Promise<void> {
|
|
136
|
+
const tsconfig = (await this.#app.workspace.readJson("tsconfig.json")) as {
|
|
137
|
+
compilerOptions: { paths: Record<string, string[]> };
|
|
138
|
+
};
|
|
139
|
+
const pathEntries: [string, string[]][] = [[`@${this.#app.name}/*`, [`apps/${this.#app.name}/*`]]];
|
|
140
|
+
for (const lib of libDeps) {
|
|
141
|
+
pathEntries.push([`@${lib}`, [`libs/${lib}/index.ts`]], [`@${lib}/*`, [`libs/${lib}/*`]]);
|
|
142
|
+
}
|
|
143
|
+
tsconfig.compilerOptions.paths = Object.fromEntries(pathEntries) as Record<string, string[]>;
|
|
144
|
+
await Bun.write(`${sourceRoot}/tsconfig.json`, JSON.stringify(tsconfig, null, 2));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#confidentialReadme(): string {
|
|
148
|
+
return `# ${this.#app.name}
|
|
149
|
+
본 프로젝트의 소스코드 및 관련자료는 모두 비밀정보로 관리됩니다.
|
|
150
|
+
|
|
151
|
+
## Get Started
|
|
152
|
+
Run the code below.
|
|
153
|
+
\`\`\`
|
|
154
|
+
bun install -g bun
|
|
155
|
+
bun i
|
|
156
|
+
|
|
157
|
+
cat <<EOF >> .env
|
|
158
|
+
# ENV For Server => debug | debug.local | develop | develop.local | main | main.local
|
|
159
|
+
SERVER_ENV=debug.local
|
|
160
|
+
# Run Mode For Server => federation | batch | all
|
|
161
|
+
SERVER_MODE=federation
|
|
162
|
+
# ENV For Client => debug | debug.local | develop | develop.local | main | main.local
|
|
163
|
+
AKAN_PUBLIC_CLIENT_ENV=debug.local
|
|
164
|
+
ANALYZE=false
|
|
165
|
+
EOF
|
|
166
|
+
|
|
167
|
+
akn start-backend ${this.#app.name}
|
|
168
|
+
# or akn start-frontend ${this.#app.name}, etc
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
## Build
|
|
172
|
+
Run the code below.
|
|
173
|
+
\`\`\`
|
|
174
|
+
akn build-backend ${this.#app.name}
|
|
175
|
+
# or akn build-frontend ${this.#app.name}, etc
|
|
176
|
+
\`\`\`
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#publicReadme(): string {
|
|
181
|
+
return `# ${this.#app.name}
|
|
182
|
+
## Get Started
|
|
183
|
+
Run the code below.
|
|
184
|
+
\`\`\`
|
|
185
|
+
cat <<EOF >> .env
|
|
186
|
+
# ENV For Server => debug | debug.local | develop | develop.local | main | main.local
|
|
187
|
+
SERVER_ENV=debug.local
|
|
188
|
+
# Run Mode For Server => federation | batch | all
|
|
189
|
+
SERVER_MODE=federation
|
|
190
|
+
# ENV For Client => debug | debug.local | develop | develop.local | main | main.local
|
|
191
|
+
AKAN_PUBLIC_CLIENT_ENV=debug.local
|
|
192
|
+
ANALYZE=false
|
|
193
|
+
EOF
|
|
194
|
+
|
|
195
|
+
akan start ${this.#app.name}
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
## Build
|
|
199
|
+
Run the code below.
|
|
200
|
+
\`\`\`
|
|
201
|
+
akan build ${this.#app.name}
|
|
202
|
+
# or akn build-frontend ${this.#app.name}, etc
|
|
203
|
+
\`\`\`
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { App } from "../commandDecorators";
|
|
4
|
+
|
|
5
|
+
export interface PageEntry {
|
|
6
|
+
key: string;
|
|
7
|
+
moduleAbsPath: string;
|
|
8
|
+
seedAbsPaths?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
|
|
12
|
+
|
|
13
|
+
async function appHasStModule(appCwdPath: string): Promise<boolean> {
|
|
14
|
+
return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts");
|
|
18
|
+
|
|
19
|
+
interface RootBoundary {
|
|
20
|
+
sourceKey: string | null;
|
|
21
|
+
sourceAbsPath: string | null;
|
|
22
|
+
segments: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getRootBoundarySegments(key: string): string[] | null {
|
|
26
|
+
const match = LAYOUT_KEY_RE.exec(key);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const prefix = match[1]?.replace(/\/$/, "");
|
|
29
|
+
if (!prefix) return [];
|
|
30
|
+
return prefix.split("/").filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function implicitRootLayoutKey(segments: string[]): string {
|
|
34
|
+
return `./${[...segments, "__root_layout"].join("/")}.tsx`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function implicitRootLayoutAbsPath(appCwdPath: string, segments: string[]): string {
|
|
38
|
+
const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
|
|
39
|
+
return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
|
|
43
|
+
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
44
|
+
if (firstVisibleIndex === -1) return segments.length <= 1;
|
|
45
|
+
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment))) return false;
|
|
46
|
+
const visible = segments.slice(firstVisibleIndex);
|
|
47
|
+
const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
|
|
48
|
+
return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: Iterable<string>): RootBoundary[] {
|
|
52
|
+
const boundaries = new Map<string, RootBoundary>();
|
|
53
|
+
for (const key of pageKeys) {
|
|
54
|
+
const segments = getRootBoundarySegments(key);
|
|
55
|
+
if (!segments) continue;
|
|
56
|
+
if (!isRootBoundarySegments(segments, basePaths)) continue;
|
|
57
|
+
const id = segments.join("/");
|
|
58
|
+
boundaries.set(id, {
|
|
59
|
+
sourceKey: key,
|
|
60
|
+
sourceAbsPath: path.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
|
|
61
|
+
segments,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
|
|
65
|
+
if (!hasExplicitRootBoundary && boundaries.size === 0) {
|
|
66
|
+
boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
|
|
67
|
+
}
|
|
68
|
+
return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function routePrefixForSegments(segments: string[]): string | null {
|
|
72
|
+
const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
|
|
73
|
+
return visible[0] ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function assertEnvClientConvention(appCwdPath: string, appName: string) {
|
|
77
|
+
const envPath = path.join(appCwdPath, "env", "env.client.ts");
|
|
78
|
+
if (!(await Bun.file(envPath).exists())) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeGeneratedRootLayoutFile(opts: {
|
|
86
|
+
appCwdPath: string;
|
|
87
|
+
appName: string;
|
|
88
|
+
boundary: RootBoundary;
|
|
89
|
+
includeStInit: boolean;
|
|
90
|
+
}): Promise<string> {
|
|
91
|
+
await assertEnvClientConvention(opts.appCwdPath, opts.appName);
|
|
92
|
+
const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
|
|
93
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
94
|
+
const sourceRel = opts.boundary.sourceAbsPath
|
|
95
|
+
? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/")
|
|
96
|
+
: null;
|
|
97
|
+
const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null;
|
|
98
|
+
const clientImport = opts.includeStInit
|
|
99
|
+
? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n`
|
|
100
|
+
: `import "@apps/${opts.appName}/client";\n`;
|
|
101
|
+
const prefix = routePrefixForSegments(opts.boundary.segments);
|
|
102
|
+
const userImport = sourceSpecifier
|
|
103
|
+
? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n`
|
|
104
|
+
: "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n";
|
|
105
|
+
const source = `import type { LayoutProps, PageProps } from "akanjs/client";\nimport { loadFonts } from "akanjs/client";\nimport { System } from "akanjs/ui";\nimport { env } from "@apps/${opts.appName}/env/env.client";\n${clientImport}${userImport}\nconst userFonts = userLayout.fonts ?? [];\nconst defaultFonts = userFonts.filter((font) => font.default);\nif (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");\nconst defaultFont = defaultFonts[0];\nconst defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;\n\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n return userLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return (\n <System.Provider\n of={GeneratedLayout as never}\n appName=${JSON.stringify(opts.appName)}\n ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}\n manifest={userLayout.manifest}\n env={env}\n theme={userLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`;
|
|
106
|
+
await Bun.write(absPath, source);
|
|
107
|
+
return absPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* When no root `page/_layout.*` exists on disk, merge a generated implicit root layout
|
|
112
|
+
* (with generated client runtime registration and optional `void st` when `lib/st.ts` exists).
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveSsrPageEntries(opts: {
|
|
115
|
+
appCwdPath: string;
|
|
116
|
+
appName: string;
|
|
117
|
+
pageKeys: string[];
|
|
118
|
+
basePaths?: Iterable<string>;
|
|
119
|
+
}): Promise<PageEntry[]> {
|
|
120
|
+
const absPageDir = path.resolve(opts.appCwdPath, "page");
|
|
121
|
+
const hasSt = await appHasStModule(opts.appCwdPath);
|
|
122
|
+
const basePaths = opts.basePaths ?? [];
|
|
123
|
+
const rootLayoutKeys = new Set(
|
|
124
|
+
opts.pageKeys.filter((key) => {
|
|
125
|
+
const segments = getRootBoundarySegments(key);
|
|
126
|
+
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
const base = opts.pageKeys
|
|
130
|
+
.filter((key) => !rootLayoutKeys.has(key))
|
|
131
|
+
.map((key) => ({
|
|
132
|
+
key,
|
|
133
|
+
moduleAbsPath: path.resolve(absPageDir, key),
|
|
134
|
+
}));
|
|
135
|
+
const generated = await Promise.all(
|
|
136
|
+
findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths).map(async (boundary) => ({
|
|
137
|
+
key: implicitRootLayoutKey(boundary.segments),
|
|
138
|
+
moduleAbsPath: await writeGeneratedRootLayoutFile({
|
|
139
|
+
appCwdPath: opts.appCwdPath,
|
|
140
|
+
appName: opts.appName,
|
|
141
|
+
boundary,
|
|
142
|
+
includeStInit: hasSt && boundary.segments.length === 0,
|
|
143
|
+
}),
|
|
144
|
+
seedAbsPaths: boundary.sourceAbsPath ? [boundary.sourceAbsPath] : [],
|
|
145
|
+
})),
|
|
146
|
+
);
|
|
147
|
+
const entries = [...base, ...generated];
|
|
148
|
+
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
149
|
+
return entries;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function resolveSsrPageEntriesForApp(app: App, pageKeys: string[]): Promise<PageEntry[]> {
|
|
153
|
+
const config = await app.getConfig();
|
|
154
|
+
return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./routeSeedIndex";
|