@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { BunPlugin } from "bun";
|
|
3
|
+
import type { App } from "../commandDecorators";
|
|
4
|
+
import { CsrDevFactoryCheck } from "./csrDevFactoryCheck";
|
|
5
|
+
import { CsrDevPaths } from "./csrDevPaths";
|
|
6
|
+
import { CsrDevResolver } from "./csrDevResolver";
|
|
7
|
+
import type { CsrDevCompiledModule, CsrDevContext, CsrDevSharedHelpers } from "./csrDevTypes";
|
|
8
|
+
import { RouteClientBuilder } from "./routeClientBuilder";
|
|
9
|
+
|
|
10
|
+
interface OutputMetafile {
|
|
11
|
+
outputs: Record<string, { entryPoint?: string; inputs: Record<string, unknown> }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BuildRound {
|
|
15
|
+
modules: CsrDevCompiledModule[];
|
|
16
|
+
misses: string[];
|
|
17
|
+
tangled: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CsrDevModuleCompilerOptions {
|
|
21
|
+
app: App;
|
|
22
|
+
paths: CsrDevPaths;
|
|
23
|
+
resolver: CsrDevResolver;
|
|
24
|
+
context: CsrDevContext;
|
|
25
|
+
outDir: string;
|
|
26
|
+
/** Where the dev server serves `outDir`: an imported asset's URL starts with it. */
|
|
27
|
+
routePrefix: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CsrDevCompileOptions {
|
|
31
|
+
/** Stop before building an npm module the graph does not know yet, and name it instead. */
|
|
32
|
+
refuseNewVendor?: boolean;
|
|
33
|
+
/** Stop at an import no recorded resolution answers, instead of running the resolution build (a whole-app build). */
|
|
34
|
+
refusePrepass?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CsrDevCompileResult {
|
|
38
|
+
modules: CsrDevCompiledModule[];
|
|
39
|
+
refusedVendors: string[];
|
|
40
|
+
unresolved: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//* Every module becomes its own CJS factory: one Bun.build with each import external, so a save re-runs only the
|
|
44
|
+
//* modules it changed instead of reloading one scope-hoisted file.
|
|
45
|
+
export class CsrDevModuleCompiler {
|
|
46
|
+
static readonly #storeRoot = /(?:^|\/)lib\/st\.ts$/;
|
|
47
|
+
static readonly #macroImport = /\b(?:with|assert)\s*\{\s*type\s*:\s*["']macro["']/;
|
|
48
|
+
static readonly #factoryArgument =
|
|
49
|
+
/(?<![\w$.])(?:require|module|exports|__akanImport|\$RefreshReg\$|\$RefreshSig\$)(?![\w$]|\s*:)/;
|
|
50
|
+
static readonly #helperPatches = [
|
|
51
|
+
["__toESM", "(mod, isNodeMode, target)", "toESM"],
|
|
52
|
+
["__reExport", "(target, mod, secondTarget)", "reExport"],
|
|
53
|
+
] as const;
|
|
54
|
+
readonly #app: App;
|
|
55
|
+
readonly #paths: CsrDevPaths;
|
|
56
|
+
readonly #resolver: CsrDevResolver;
|
|
57
|
+
readonly #context: CsrDevContext;
|
|
58
|
+
readonly #outDir: string;
|
|
59
|
+
readonly #routePrefix: string;
|
|
60
|
+
readonly #assets = new Set<string>();
|
|
61
|
+
|
|
62
|
+
constructor({ app, paths, resolver, context, outDir, routePrefix }: CsrDevModuleCompilerOptions) {
|
|
63
|
+
this.#app = app;
|
|
64
|
+
this.#paths = paths;
|
|
65
|
+
this.#resolver = resolver;
|
|
66
|
+
this.#context = context;
|
|
67
|
+
this.#outDir = outDir;
|
|
68
|
+
this.#routePrefix = routePrefix;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every asset file this compiler wrote into the registry directory. */
|
|
72
|
+
get assets(): string[] {
|
|
73
|
+
return [...this.#assets];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async compile(
|
|
77
|
+
startFiles: string[],
|
|
78
|
+
known: Set<string>,
|
|
79
|
+
{ refuseNewVendor = false, refusePrepass = false }: CsrDevCompileOptions = {},
|
|
80
|
+
): Promise<CsrDevCompileResult> {
|
|
81
|
+
const compiled = new Map<string, CsrDevCompiledModule>();
|
|
82
|
+
const seen = new Set([...known, ...startFiles]);
|
|
83
|
+
let frontier = [...new Set(startFiles)];
|
|
84
|
+
while (frontier.length > 0) {
|
|
85
|
+
const refusedVendors = refuseNewVendor ? frontier.filter((file) => CsrDevPaths.isVendorFile(file)) : [];
|
|
86
|
+
if (refusedVendors.length > 0) return { modules: [...compiled.values()], refusedVendors, unresolved: [] };
|
|
87
|
+
await this.#resolver.cover(frontier);
|
|
88
|
+
const round = await this.#compileRound(frontier);
|
|
89
|
+
if (round.misses.length > 0 && !this.#resolver.prepassDone && refusePrepass)
|
|
90
|
+
return { modules: [...compiled.values()], refusedVendors: [], unresolved: round.misses };
|
|
91
|
+
if (round.misses.length > 0 && !this.#resolver.prepassDone) {
|
|
92
|
+
this.#app.verbose(`[csr-dev] ${round.misses.length} unresolved import(s); rerunning the resolution build`);
|
|
93
|
+
await this.#resolver.prepass();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const next: string[] = [];
|
|
97
|
+
for (const module of round.modules) {
|
|
98
|
+
compiled.set(module.file, module);
|
|
99
|
+
for (const dep of module.deps) {
|
|
100
|
+
if (CsrDevPaths.isStub(dep) || seen.has(dep)) continue;
|
|
101
|
+
seen.add(dep);
|
|
102
|
+
next.push(dep);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
frontier = next;
|
|
106
|
+
}
|
|
107
|
+
const { fallbacks } = this.#resolver;
|
|
108
|
+
if (fallbacks.size > 0)
|
|
109
|
+
this.#app.verbose(
|
|
110
|
+
`[csr-dev] ${fallbacks.size} import(s) outside the browser build's graph resolved with Bun's runtime resolver: ${[...fallbacks].slice(0, 5).join(", ")}${fallbacks.size > 5 ? ", ..." : ""}`,
|
|
111
|
+
);
|
|
112
|
+
return { modules: [...compiled.values()], refusedVendors: [], unresolved: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async #compileRound(files: string[]): Promise<Omit<BuildRound, "tangled">> {
|
|
116
|
+
const appFiles = files.filter((file) => !CsrDevPaths.isVendorFile(file));
|
|
117
|
+
const vendorFiles = files.filter((file) => CsrDevPaths.isVendorFile(file));
|
|
118
|
+
const rounds = await Promise.all([
|
|
119
|
+
appFiles.length > 0 ? this.#bunBuild(appFiles, false) : null,
|
|
120
|
+
vendorFiles.length > 0 ? this.#bunBuild(vendorFiles, true) : null,
|
|
121
|
+
]);
|
|
122
|
+
const modules = rounds.flatMap((round) => round?.modules ?? []);
|
|
123
|
+
const misses = rounds.flatMap((round) => round?.misses ?? []);
|
|
124
|
+
// One entrypoint per build cannot inline another: Bun ignores an external for an absolute import of an entrypoint.
|
|
125
|
+
for (const file of rounds.flatMap((round) => round?.tangled ?? [])) {
|
|
126
|
+
const single = await this.#bunBuild([file], CsrDevPaths.isVendorFile(file));
|
|
127
|
+
if (single.tangled.length > 0)
|
|
128
|
+
throw new Error(`[csr-dev] ${this.#paths.idOf(file)} kept inlining another module`);
|
|
129
|
+
modules.push(...single.modules);
|
|
130
|
+
misses.push(...single.misses);
|
|
131
|
+
}
|
|
132
|
+
return { modules, misses };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async #bunBuild(files: string[], vendor: boolean): Promise<BuildRound> {
|
|
136
|
+
//? Keyed as the output is: by the disk's spelling, which a case-only rename leaves apart from the requested path.
|
|
137
|
+
const mtimes = new Map(files.map((file) => [CsrDevPaths.realpath(file), CsrDevPaths.mtimeOf(file)]));
|
|
138
|
+
const hashes = new Map(
|
|
139
|
+
await Promise.all(
|
|
140
|
+
files.map(async (file) => [CsrDevPaths.realpath(file), vendor ? "" : await CsrDevPaths.hashOf(file)] as const),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
const misses: string[] = [];
|
|
144
|
+
const macroOutputs = vendor ? new Map<string, string>() : await this.#evaluateMacros(files);
|
|
145
|
+
let result: Awaited<ReturnType<typeof Bun.build>>;
|
|
146
|
+
try {
|
|
147
|
+
result = await Bun.build({
|
|
148
|
+
entrypoints: files,
|
|
149
|
+
root: this.#paths.root,
|
|
150
|
+
target: "browser",
|
|
151
|
+
format: "cjs",
|
|
152
|
+
reactFastRefresh: !vendor,
|
|
153
|
+
sourcemap: vendor ? "none" : "external",
|
|
154
|
+
naming: { entry: "[dir]/[name].[ext]", asset: "assets/[name]-[hash].[ext]" },
|
|
155
|
+
publicPath: this.#routePrefix,
|
|
156
|
+
metafile: true,
|
|
157
|
+
env: "AKAN_PUBLIC_*",
|
|
158
|
+
//? A factory runs as a classic script, where `import.meta` is a SyntaxError that takes the whole vendor file
|
|
159
|
+
//? down, and Bun's cjs output keeps `import.meta.env` and spells `import.meta.url` as this disk's `file://` path.
|
|
160
|
+
define: { ...this.#context.define, "import.meta": "__akanMeta" },
|
|
161
|
+
optimizeImports: this.#context.optimizeImports,
|
|
162
|
+
plugins: [this.#registryPlugin(vendor, misses, macroOutputs)],
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (vendor && error instanceof AggregateError) return await this.#withoutFailed(files, error);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const { outputs } = result.metafile as unknown as OutputMetafile;
|
|
169
|
+
const sourceMaps = new Map(
|
|
170
|
+
result.outputs.filter((artifact) => artifact.kind === "sourcemap").map((artifact) => [artifact.path, artifact]),
|
|
171
|
+
);
|
|
172
|
+
const modules: CsrDevCompiledModule[] = [];
|
|
173
|
+
const tangled: string[] = [];
|
|
174
|
+
for (const artifact of result.outputs) {
|
|
175
|
+
if (artifact.kind === "asset") {
|
|
176
|
+
const assetPath = path.join(this.#outDir, artifact.path);
|
|
177
|
+
await Bun.write(assetPath, artifact);
|
|
178
|
+
this.#assets.add(assetPath);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (artifact.kind !== "entry-point") continue;
|
|
182
|
+
const meta = outputs[artifact.path];
|
|
183
|
+
if (!meta?.entryPoint) throw new Error(`[csr-dev] no entry point recorded for ${artifact.path}`);
|
|
184
|
+
const file = CsrDevPaths.realpath(path.resolve(meta.entryPoint));
|
|
185
|
+
const inputs = Object.keys(meta.inputs).map((input) => CsrDevPaths.realpath(path.resolve(input)));
|
|
186
|
+
if (inputs.some((input) => input !== file && this.#resolver.isRegistryModule(input))) {
|
|
187
|
+
tangled.push(file);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const code = await artifact.text();
|
|
191
|
+
const id = this.#paths.idOf(file);
|
|
192
|
+
const built = CsrDevModuleCompiler.#factory(id, code);
|
|
193
|
+
const problem = CsrDevFactoryCheck.problemOf(built.factory, built.helpers?.definition);
|
|
194
|
+
if (problem && !vendor)
|
|
195
|
+
throw new Error(`[csr-dev] ${id}: its factory does not parse as a classic script: ${problem}`);
|
|
196
|
+
if (problem) {
|
|
197
|
+
modules.push(this.#leftOut(file, problem));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const sourceMap = sourceMaps.get(`${artifact.path}.map`);
|
|
201
|
+
modules.push({
|
|
202
|
+
id,
|
|
203
|
+
file,
|
|
204
|
+
vendor,
|
|
205
|
+
...built,
|
|
206
|
+
sourceMap: sourceMap ? this.#rebaseSourceMap(await sourceMap.text()) : undefined,
|
|
207
|
+
deps: this.#emittedDeps(code),
|
|
208
|
+
mtimeMs: mtimes.get(file) ?? CsrDevPaths.mtimeOf(file),
|
|
209
|
+
hash: hashes.get(file) ?? "",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return { modules, misses, tangled };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//? A package file that cannot be a CommonJS factory would fail every module built beside it, and no save fixes a
|
|
216
|
+
//? package: it is left out and named in the log, and the module requiring it gets the reason. App code still fails.
|
|
217
|
+
async #withoutFailed(files: string[], error: AggregateError): Promise<BuildRound> {
|
|
218
|
+
const reasons = new Map<string, string[]>();
|
|
219
|
+
for (const message of error.errors as {
|
|
220
|
+
message?: unknown;
|
|
221
|
+
position?: { file?: unknown; line?: number } | null;
|
|
222
|
+
}[]) {
|
|
223
|
+
const file = message.position?.file;
|
|
224
|
+
if (typeof file !== "string" || !path.isAbsolute(file)) continue;
|
|
225
|
+
const real = CsrDevPaths.realpath(file);
|
|
226
|
+
const at = message.position?.line ? `:${message.position.line}` : "";
|
|
227
|
+
reasons.set(real, [...(reasons.get(real) ?? []), `${String(message.message)} (${this.#paths.idOf(real)}${at})`]);
|
|
228
|
+
}
|
|
229
|
+
const failed = new Set(files.filter((file) => reasons.has(CsrDevPaths.realpath(file))));
|
|
230
|
+
if (failed.size === 0) throw error;
|
|
231
|
+
const rest = files.filter((file) => !failed.has(file));
|
|
232
|
+
const round = rest.length > 0 ? await this.#bunBuild(rest, true) : { modules: [], misses: [], tangled: [] };
|
|
233
|
+
for (const file of failed) {
|
|
234
|
+
const text = (reasons.get(CsrDevPaths.realpath(file)) ?? []).join("; ");
|
|
235
|
+
const awaits = text.includes('"await" can only be used');
|
|
236
|
+
round.modules.push(
|
|
237
|
+
this.#leftOut(file, awaits ? `a top-level await, which a CommonJS factory cannot wait on: ${text}` : text),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return round;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#leftOut(file: string, problem: string): CsrDevCompiledModule {
|
|
244
|
+
const real = CsrDevPaths.realpath(file);
|
|
245
|
+
const id = this.#paths.idOf(real);
|
|
246
|
+
this.#app.logger.error(`[csr-dev] ${id} is left out of the dev registry: ${problem}`);
|
|
247
|
+
return {
|
|
248
|
+
id,
|
|
249
|
+
file: real,
|
|
250
|
+
vendor: true,
|
|
251
|
+
factory: CsrDevFactoryCheck.thrower(id, problem),
|
|
252
|
+
helpers: null,
|
|
253
|
+
deps: [],
|
|
254
|
+
mtimeMs: CsrDevPaths.mtimeOf(real),
|
|
255
|
+
hash: "",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//? Bun applies `define` to the modules a macro imports too, and runs them in this process, where the `import.meta`
|
|
260
|
+
//? rewrite leaves a free `__akanMeta` that no factory header declares. So a file importing a macro has its macros run
|
|
261
|
+
//? first by a build of its own, with every other import left as written, and the module build reads that output.
|
|
262
|
+
async #evaluateMacros(files: string[]): Promise<Map<string, string>> {
|
|
263
|
+
const sources = await Promise.all(files.map(async (file) => [file, await Bun.file(file).text()] as const));
|
|
264
|
+
const importers = sources.filter(([, source]) => CsrDevModuleCompiler.#macroImport.test(source));
|
|
265
|
+
const outputs = await Promise.all(
|
|
266
|
+
importers.map(async ([file]) => {
|
|
267
|
+
const result = await Bun.build({
|
|
268
|
+
entrypoints: [file],
|
|
269
|
+
target: "browser",
|
|
270
|
+
format: "esm",
|
|
271
|
+
plugins: [
|
|
272
|
+
{
|
|
273
|
+
name: "akan-csr-macro-pass",
|
|
274
|
+
setup: (build) =>
|
|
275
|
+
void build.onResolve({ filter: /.*/ }, (args) =>
|
|
276
|
+
args.importer ? { path: args.path, external: true } : undefined,
|
|
277
|
+
),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
const [output] = result.outputs;
|
|
282
|
+
if (!output) throw new Error(`[csr-dev] ${this.#paths.idOf(file)}: its macro pass wrote no output`);
|
|
283
|
+
const text = await output.text();
|
|
284
|
+
return [
|
|
285
|
+
[file, text],
|
|
286
|
+
[CsrDevPaths.realpath(file), text],
|
|
287
|
+
] as const;
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
return new Map(outputs.flat());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#registryPlugin(vendor: boolean, misses: string[], macroOutputs: Map<string, string>): BunPlugin {
|
|
294
|
+
return {
|
|
295
|
+
name: "akan-csr-registry",
|
|
296
|
+
setup: (build) => {
|
|
297
|
+
build.onLoad({ filter: /\.css$/ }, () => ({ contents: "", loader: "js" }));
|
|
298
|
+
if (macroOutputs.size > 0) {
|
|
299
|
+
const exact = [...macroOutputs.keys()].map((file) => file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
300
|
+
build.onLoad({ filter: new RegExp(`^(?:${exact.join("|")})$`) }, (args) => {
|
|
301
|
+
const contents = macroOutputs.get(args.path);
|
|
302
|
+
return contents === undefined ? undefined : { contents, loader: "js" };
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (!vendor)
|
|
306
|
+
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
|
|
307
|
+
if (CsrDevPaths.isVendorFile(args.path)) return undefined;
|
|
308
|
+
const normalized = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(
|
|
309
|
+
await Bun.file(args.path).text(),
|
|
310
|
+
{ path: args.path },
|
|
311
|
+
);
|
|
312
|
+
return normalized ? { contents: normalized, loader: "tsx" } : undefined;
|
|
313
|
+
});
|
|
314
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
315
|
+
if (args.kind === "entry-point-build" || !args.importer) return undefined;
|
|
316
|
+
if (this.#context.externals?.includes(args.path))
|
|
317
|
+
return { path: `${CsrDevPaths.vendorPrefix}${args.path}`, external: true };
|
|
318
|
+
const importer = CsrDevPaths.realpath(args.importer);
|
|
319
|
+
const target = this.#resolver.resolve(
|
|
320
|
+
importer,
|
|
321
|
+
args.path,
|
|
322
|
+
args.kind === "require-call" || args.kind === "require-resolve" ? "require" : "import",
|
|
323
|
+
);
|
|
324
|
+
if (target === CsrDevResolver.inline) return undefined;
|
|
325
|
+
if (target === null) {
|
|
326
|
+
misses.push(`${args.path} from ${this.#paths.idOf(importer)}`);
|
|
327
|
+
return { path: `akan-miss:${args.path}`, external: true };
|
|
328
|
+
}
|
|
329
|
+
const external = CsrDevPaths.isStub(target)
|
|
330
|
+
? target
|
|
331
|
+
: `${CsrDevPaths.modulePrefix}${this.#paths.idOf(target)}`;
|
|
332
|
+
return { path: external, external: true };
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
//? Bun writes sources relative to the working directory; the page shows them by workspace path instead. A package
|
|
339
|
+
//? barrel inlined into its importer (react-icons) would carry its whole text into every importer's map.
|
|
340
|
+
#rebaseSourceMap(text: string): string {
|
|
341
|
+
const map = JSON.parse(text) as { sources: string[]; sourcesContent?: (string | null)[] };
|
|
342
|
+
const files = map.sources.map((source) => path.resolve(source));
|
|
343
|
+
map.sources = files.map((file) => `/${this.#paths.idOf(file)}`);
|
|
344
|
+
map.sourcesContent = files.map((file, index) =>
|
|
345
|
+
CsrDevPaths.isVendorFile(file) ? null : (map.sourcesContent?.[index] ?? null),
|
|
346
|
+
);
|
|
347
|
+
return JSON.stringify(map);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Read off the emitted code rather than onResolve: Bun resolves both arms of `NODE_ENV ? require(a) : require(b)`
|
|
351
|
+
// before dropping the dead one, and following it would pull every package's production build in too.
|
|
352
|
+
#emittedDeps(code: string): string[] {
|
|
353
|
+
const deps = new Set<string>();
|
|
354
|
+
const references = /\b(?:require|import|__akanImport)\("((?:akan-module|stub):[^"]+)"\)/g;
|
|
355
|
+
for (const [, reference] of code.matchAll(references)) {
|
|
356
|
+
if (!reference) continue;
|
|
357
|
+
deps.add(
|
|
358
|
+
CsrDevPaths.isStub(reference)
|
|
359
|
+
? reference
|
|
360
|
+
: this.#paths.fileOf(reference.slice(CsrDevPaths.modulePrefix.length)),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return [...deps];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Every rewrite keeps the line count, so a module's source map still lines up inside the factory.
|
|
367
|
+
static #factory(id: string, code: string): { factory: string; helpers: CsrDevSharedHelpers | null } {
|
|
368
|
+
const prefix = CsrDevPaths.modulePrefix;
|
|
369
|
+
//? Bun keeps a package's `#!` line atop its output, where a script inside a function cannot have one.
|
|
370
|
+
let patched = code
|
|
371
|
+
.replace(/^#!/, "//")
|
|
372
|
+
.replaceAll(`import("${prefix}`, `__akanImport("${prefix}`)
|
|
373
|
+
.replace(/^\/\/# debugId=.*$/m, "");
|
|
374
|
+
//? A file esbuild already bundled (mermaid's parser chunks) defines its own helper under the same name, below Bun's
|
|
375
|
+
//? preamble, and Bun then emits none. Only a copy there is exempt: one in the preamble is Bun's own, changed.
|
|
376
|
+
const marker = patched.search(/^\/\/ /m);
|
|
377
|
+
const body = marker < 0 ? "" : patched.slice(marker);
|
|
378
|
+
for (const [name, params, method] of CsrDevModuleCompiler.#helperPatches) {
|
|
379
|
+
const definition = `var ${name} = ${params} => {`;
|
|
380
|
+
if (patched.includes(definition))
|
|
381
|
+
patched = patched.replace(definition, `var ${name} = __akan.${method}, __bun${name} = ${params} => {`);
|
|
382
|
+
else if (patched.includes(`${name}(`) && !new RegExp(`\\b(?:var|let|const|function)\\s+${name}\\b`).test(body))
|
|
383
|
+
throw new Error(
|
|
384
|
+
`[csr-dev] ${id}: Bun's ${name} helper no longer reads "${definition}"; CsrDevModuleCompiler must follow`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
// StoreRegistry.build merges into one global store instance, so every importer already holds the updated `st`:
|
|
388
|
+
// re-running the store root is the whole update, and bubbling past it would only reach the page modules.
|
|
389
|
+
if (CsrDevModuleCompiler.#storeRoot.test(id)) patched += "\nmodule.hot.accept();";
|
|
390
|
+
const shared = CsrDevModuleCompiler.#shareHelpers(patched);
|
|
391
|
+
//? What `import.meta` is in a browser module: a `url` and no `env`, so `import.meta.env` checks (jotai, zustand)
|
|
392
|
+
//? branch as they do in the ESM bundle. On the header line, outside the preamble that modules share.
|
|
393
|
+
const meta = /(?<![\w$.])__akanMeta(?![\w$])/.test(patched)
|
|
394
|
+
? ` var __akanMeta = { url: new URL(${JSON.stringify(id.startsWith("/") ? id : `/${id}`)}, self.location.href).href };`
|
|
395
|
+
: "";
|
|
396
|
+
return {
|
|
397
|
+
factory: `function (require, module, exports, $RefreshReg$, $RefreshSig$, __akanImport) {${meta}\n${shared?.code ?? patched}\n}`,
|
|
398
|
+
helpers: shared?.helpers ?? null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
//? Bun's interop helpers are everything before its first `// <path>` comment. One naming a factory argument would
|
|
403
|
+
//? bind to whichever module ran it first, so only a self-contained preamble is shared.
|
|
404
|
+
static #shareHelpers(code: string): { code: string; helpers: CsrDevSharedHelpers } | null {
|
|
405
|
+
const marker = code.search(/^\/\/ /m);
|
|
406
|
+
if (marker <= 0) return null;
|
|
407
|
+
const preamble = code.slice(0, marker);
|
|
408
|
+
const names = [...preamble.matchAll(/^(?:var|let|const|function)\s+([\w$]+)/gm)]
|
|
409
|
+
.map(([, name]) => name)
|
|
410
|
+
.filter((name): name is string => !!name);
|
|
411
|
+
if (names.length === 0 || CsrDevModuleCompiler.#factoryArgument.test(preamble)) return null;
|
|
412
|
+
const hash = Bun.hash(preamble).toString(36);
|
|
413
|
+
const binding = `var { ${names.join(", ")} } = __akan.helpers(${JSON.stringify(hash)});`;
|
|
414
|
+
return {
|
|
415
|
+
code: `${binding}${"\n".repeat(CsrDevPaths.lineCount(preamble))}${code.slice(marker)}`,
|
|
416
|
+
helpers: {
|
|
417
|
+
hash,
|
|
418
|
+
definition: `__akan.defineHelpers(${JSON.stringify(hash)}, function () {\n${preamble}return { ${names.join(", ")} };\n});\n`,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|