@codemation/cli 0.0.15 → 0.0.16

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.
@@ -374,6 +374,7 @@ export class ConsumerOutputBuilder {
374
374
  sourcePath,
375
375
  });
376
376
  }
377
+ await this.emitConfigSourceFile(outputAppRoot, configSourcePath, runtimeSourcePaths);
377
378
  },
378
379
  });
379
380
  }
@@ -463,6 +464,24 @@ export class ConsumerOutputBuilder {
463
464
  }
464
465
  }
465
466
 
467
+ private async emitConfigSourceFile(
468
+ outputAppRoot: string,
469
+ configSourcePath: string,
470
+ runtimeSourcePaths: ReadonlyArray<string>,
471
+ ): Promise<void> {
472
+ const normalizedConfigSourcePath = path.resolve(configSourcePath);
473
+ const alreadyEmitted = runtimeSourcePaths.some(
474
+ (sourcePath) => path.resolve(sourcePath) === normalizedConfigSourcePath,
475
+ );
476
+ if (alreadyEmitted) {
477
+ return;
478
+ }
479
+ await this.emitSourceFile({
480
+ outputAppRoot,
481
+ sourcePath: normalizedConfigSourcePath,
482
+ });
483
+ }
484
+
466
485
  private createCompilerOptions(): ts.CompilerOptions {
467
486
  const scriptTarget = this.buildOptions.target === "es2020" ? ts.ScriptTarget.ES2020 : ts.ScriptTarget.ES2022;
468
487
  return {
@@ -0,0 +1,16 @@
1
+ import type { Logger } from "@codemation/host/next/server";
2
+
3
+ import { ConsumerOutputBuilder } from "./ConsumerOutputBuilder";
4
+ import type { ConsumerBuildOptions } from "./consumerBuildOptions.types";
5
+
6
+ export class ConsumerOutputBuilderFactory {
7
+ create(
8
+ consumerRoot: string,
9
+ args?: Readonly<{
10
+ buildOptions?: ConsumerBuildOptions;
11
+ logger?: Logger;
12
+ }>,
13
+ ): ConsumerOutputBuilder {
14
+ return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions);
15
+ }
16
+ }
@@ -4,13 +4,13 @@ import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
4
4
  import { ListenPortResolver } from "../runtime/ListenPortResolver";
5
5
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
6
6
 
7
- import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
8
7
  import { DevHttpProbe } from "./DevHttpProbe";
9
8
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
10
9
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
11
10
  import { DevSessionServices } from "./DevSessionServices";
12
11
  import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
13
12
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
13
+ import { NextHostEdgeSeedLoader } from "./NextHostEdgeSeedLoader";
14
14
  import { WatchRootsResolver } from "./WatchRootsResolver";
15
15
 
16
16
  export class DevSessionServicesBuilder {
@@ -25,7 +25,7 @@ export class DevSessionServicesBuilder {
25
25
  new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator),
26
26
  loopbackPortAllocator,
27
27
  new DevHttpProbe(),
28
- new DevAuthSettingsLoader(new CodemationConsumerConfigLoader(), consumerEnvLoader),
28
+ new NextHostEdgeSeedLoader(new CodemationConsumerConfigLoader(), consumerEnvLoader),
29
29
  new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions),
30
30
  new WatchRootsResolver(),
31
31
  new DevSourceChangeClassifier(),
@@ -21,6 +21,7 @@ export class DevApiRuntimeFactory {
21
21
  httpPort,
22
22
  workflowWebSocketPort,
23
23
  new DevApiRuntimeHost(this.configLoader, this.pluginDiscovery, {
24
+ configPathOverride: args.configPathOverride,
24
25
  consumerRoot: args.consumerRoot,
25
26
  env: {
26
27
  ...args.env,
@@ -1,4 +1,5 @@
1
- import type { CodemationPlugin } from "@codemation/host";
1
+ import type { AppConfig, AppPluginLoadSummary, CodemationPlugin } from "@codemation/host";
2
+ import { CodemationPluginPackageMetadata } from "@codemation/host";
2
3
  import {
3
4
  AppContainerFactory,
4
5
  AppContainerLifecycle,
@@ -18,13 +19,15 @@ import { CodemationTsyringeTypeInfoRegistrar } from "@codemation/host-src/presen
18
19
  import type { DevApiRuntimeContext } from "./DevApiRuntimeTypes";
19
20
 
20
21
  export class DevApiRuntimeHost {
21
- private readonly pluginListMerger = new CodemationPluginListMerger();
22
+ private readonly pluginPackageMetadata = new CodemationPluginPackageMetadata();
23
+ private readonly pluginListMerger = new CodemationPluginListMerger(this.pluginPackageMetadata);
22
24
  private contextPromise: Promise<DevApiRuntimeContext> | null = null;
23
25
 
24
26
  constructor(
25
27
  private readonly configLoader: AppConfigLoader,
26
28
  private readonly pluginDiscovery: CodemationPluginDiscovery,
27
29
  private readonly args: Readonly<{
30
+ configPathOverride?: string;
28
31
  consumerRoot: string;
29
32
  env: NodeJS.ProcessEnv;
30
33
  runtimeWorkingDirectory: string;
@@ -62,17 +65,24 @@ export class DevApiRuntimeHost {
62
65
  env.CODEMATION_CONSUMER_ROOT = consumerRoot;
63
66
  const configResolution = await this.configLoader.load({
64
67
  consumerRoot,
68
+ configPathOverride: this.args.configPathOverride,
65
69
  repoRoot,
66
70
  env,
67
71
  });
68
72
  const discoveredPlugins = await this.loadDiscoveredPlugins(consumerRoot);
73
+ const mergedPlugins =
74
+ discoveredPlugins.length > 0
75
+ ? this.pluginListMerger.merge(configResolution.appConfig.plugins, discoveredPlugins)
76
+ : configResolution.appConfig.plugins;
69
77
  const appConfig = {
70
78
  ...configResolution.appConfig,
71
79
  env,
72
- plugins:
73
- discoveredPlugins.length > 0
74
- ? this.pluginListMerger.merge(configResolution.appConfig.plugins, discoveredPlugins)
75
- : configResolution.appConfig.plugins,
80
+ plugins: mergedPlugins,
81
+ pluginLoadSummary: this.createPluginLoadSummary(
82
+ configResolution.appConfig.plugins,
83
+ discoveredPlugins,
84
+ mergedPlugins,
85
+ ),
76
86
  };
77
87
  const container = await new AppContainerFactory().create({
78
88
  appConfig,
@@ -96,6 +106,27 @@ export class DevApiRuntimeHost {
96
106
  return resolvedPackages.map((resolvedPackage: CodemationResolvedPluginPackage) => resolvedPackage.plugin);
97
107
  }
98
108
 
109
+ private createPluginLoadSummary(
110
+ configuredPlugins: ReadonlyArray<CodemationPlugin>,
111
+ discoveredPlugins: ReadonlyArray<CodemationPlugin>,
112
+ mergedPlugins: ReadonlyArray<CodemationPlugin>,
113
+ ): AppConfig["pluginLoadSummary"] {
114
+ const configuredPluginSet = new Set(configuredPlugins);
115
+ const discoveredPluginSet = new Set(discoveredPlugins);
116
+ const summaries: AppPluginLoadSummary[] = [];
117
+ for (const plugin of mergedPlugins) {
118
+ const packageName = this.pluginPackageMetadata.readPackageName(plugin);
119
+ if (!packageName) {
120
+ continue;
121
+ }
122
+ summaries.push({
123
+ packageName,
124
+ source: configuredPluginSet.has(plugin) || !discoveredPluginSet.has(plugin) ? "configured" : "discovered",
125
+ });
126
+ }
127
+ return summaries;
128
+ }
129
+
99
130
  private async detectWorkspaceRoot(startDirectory: string): Promise<string> {
100
131
  let currentDirectory = path.resolve(startDirectory);
101
132
  while (true) {
@@ -10,6 +10,7 @@ export type DevApiRuntimeContext = Readonly<{
10
10
  }>;
11
11
 
12
12
  export type DevApiRuntimeFactoryArgs = Readonly<{
13
+ configPathOverride?: string;
13
14
  consumerRoot: string;
14
15
  env: NodeJS.ProcessEnv;
15
16
  runtimeWorkingDirectory: string;
@@ -39,8 +39,9 @@ export class DevCliBannerRenderer {
39
39
  title: chalk.bold("Runtime"),
40
40
  titleAlignment: "center",
41
41
  });
42
+ const pluginsSection = this.buildPluginsSection(summary);
42
43
  const activeSection = this.buildActiveWorkflowsSection(summary);
43
- process.stdout.write(`${detailBox}\n${activeSection}\n`);
44
+ process.stdout.write(`${detailBox}\n${pluginsSection}\n${activeSection}\n`);
44
45
  }
45
46
 
46
47
  renderFull(summary: DevBootstrapSummaryJson): void {
@@ -62,8 +63,9 @@ export class DevCliBannerRenderer {
62
63
  title: chalk.bold("Runtime (updated)"),
63
64
  titleAlignment: "center",
64
65
  });
66
+ const pluginsSection = this.buildPluginsSection(summary);
65
67
  const activeSection = this.buildActiveWorkflowsSection(summary);
66
- process.stdout.write(`\n${detailBox}\n${activeSection}\n`);
68
+ process.stdout.write(`\n${detailBox}\n${pluginsSection}\n${activeSection}\n`);
67
69
  }
68
70
 
69
71
  private renderFigletTitle(): string {
@@ -103,4 +105,21 @@ export class DevCliBannerRenderer {
103
105
  titleAlignment: "left",
104
106
  });
105
107
  }
108
+
109
+ private buildPluginsSection(summary: DevBootstrapSummaryJson): string {
110
+ const lines =
111
+ summary.plugins.length === 0
112
+ ? [chalk.dim(" (none discovered or configured)")]
113
+ : summary.plugins.map(
114
+ (plugin) => `${chalk.whiteBright(` • ${plugin.packageName} `)}${chalk.dim(`(${plugin.source})`)}`,
115
+ );
116
+ return boxen(lines.join("\n"), {
117
+ padding: { top: 0, bottom: 0, left: 0, right: 0 },
118
+ margin: { top: 1, bottom: 0 },
119
+ borderStyle: "single",
120
+ borderColor: "cyan",
121
+ title: chalk.bold("Plugins"),
122
+ titleAlignment: "left",
123
+ });
124
+ }
106
125
  }
@@ -1,7 +1,4 @@
1
- import path from "node:path";
2
1
  import process from "node:process";
3
- import type { CodemationAuthConfig } from "@codemation/host";
4
- import { CodemationFrontendAuthSnapshotFactory, FrontendAppConfigJsonCodec } from "@codemation/host";
5
2
 
6
3
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
7
4
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
@@ -10,14 +7,12 @@ export class DevNextHostEnvironmentBuilder {
10
7
  constructor(
11
8
  private readonly consumerEnvLoader: ConsumerEnvLoader,
12
9
  private readonly sourceMapNodeOptions: SourceMapNodeOptions,
13
- private readonly frontendAuthSnapshotFactory: CodemationFrontendAuthSnapshotFactory = new CodemationFrontendAuthSnapshotFactory(),
14
- private readonly frontendAppConfigJsonCodec: FrontendAppConfigJsonCodec = new FrontendAppConfigJsonCodec(),
15
10
  ) {}
16
11
 
17
12
  buildConsumerUiProxy(
18
13
  args: Readonly<{
19
- authConfigJson: string;
20
14
  authSecret: string;
15
+ configPathOverride?: string;
21
16
  consumerRoot: string;
22
17
  developmentServerToken: string;
23
18
  nextPort: number;
@@ -25,20 +20,18 @@ export class DevNextHostEnvironmentBuilder {
25
20
  runtimeDevUrl: string;
26
21
  skipUiAuth: boolean;
27
22
  websocketPort: number;
28
- consumerOutputManifestPath?: string;
29
23
  }>,
30
24
  ): NodeJS.ProcessEnv {
31
25
  return {
32
26
  ...this.build({
33
- authConfigJson: args.authConfigJson,
34
27
  authSecret: args.authSecret,
28
+ configPathOverride: args.configPathOverride,
35
29
  consumerRoot: args.consumerRoot,
36
30
  developmentServerToken: args.developmentServerToken,
37
31
  nextPort: args.nextPort,
38
32
  runtimeDevUrl: args.runtimeDevUrl,
39
33
  skipUiAuth: args.skipUiAuth,
40
34
  websocketPort: args.websocketPort,
41
- consumerOutputManifestPath: args.consumerOutputManifestPath,
42
35
  }),
43
36
  // Standalone `server.js` uses `process.env.HOSTNAME || '0.0.0.0'` for bind; Docker sets HOSTNAME to the
44
37
  // container id, which breaks loopback health checks — force IPv4 loopback for the UI child only.
@@ -50,44 +43,22 @@ export class DevNextHostEnvironmentBuilder {
50
43
 
51
44
  build(
52
45
  args: Readonly<{
53
- authConfigJson: string;
54
46
  authSecret?: string;
47
+ configPathOverride?: string;
55
48
  consumerRoot: string;
56
49
  developmentServerToken: string;
57
50
  nextPort: number;
58
51
  skipUiAuth: boolean;
59
52
  websocketPort: number;
60
53
  runtimeDevUrl?: string;
61
- /** Same manifest as `codemation build` / serve-web so @codemation/next-host can load consumer config (whitelabel, etc.). */
62
- consumerOutputManifestPath?: string;
63
54
  }>,
64
55
  ): NodeJS.ProcessEnv {
65
56
  const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process.env);
66
- const manifestPath =
67
- args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
68
- const authSecret = args.authSecret ?? merged.AUTH_SECRET;
69
- const authSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
70
- authConfig: this.parseAuthConfig(args.authConfigJson),
71
- env: {
72
- ...merged,
73
- ...(typeof authSecret === "string" && authSecret.trim().length > 0
74
- ? {
75
- AUTH_SECRET: authSecret,
76
- }
77
- : {}),
78
- },
79
- uiAuthEnabled: !args.skipUiAuth,
80
- });
81
57
  return {
82
58
  ...merged,
83
59
  PORT: String(args.nextPort),
84
60
  CODEMATION_CONSUMER_ROOT: args.consumerRoot,
85
- CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifestPath,
86
- CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
87
- auth: authSnapshot,
88
- productName: "Codemation",
89
- logoUrl: null,
90
- }),
61
+ CODEMATION_UI_AUTH_ENABLED: String(!args.skipUiAuth),
91
62
  CODEMATION_WS_PORT: String(args.websocketPort),
92
63
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
93
64
  CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
@@ -95,17 +66,13 @@ export class DevNextHostEnvironmentBuilder {
95
66
  NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
96
67
  WS_NO_BUFFER_UTIL: "1",
97
68
  WS_NO_UTF_8_VALIDATE: "1",
69
+ ...(args.authSecret && args.authSecret.trim().length > 0 ? { AUTH_SECRET: args.authSecret.trim() } : {}),
70
+ ...(args.configPathOverride && args.configPathOverride.trim().length > 0
71
+ ? { CODEMATION_CONFIG_PATH: args.configPathOverride }
72
+ : {}),
98
73
  ...(args.runtimeDevUrl !== undefined && args.runtimeDevUrl.trim().length > 0
99
74
  ? { CODEMATION_RUNTIME_DEV_URL: args.runtimeDevUrl.trim() }
100
75
  : {}),
101
76
  };
102
77
  }
103
-
104
- private parseAuthConfig(authConfigJson: string): CodemationAuthConfig | undefined {
105
- if (authConfigJson.trim().length === 0) {
106
- return undefined;
107
- }
108
- const parsed = JSON.parse(authConfigJson) as CodemationAuthConfig | null;
109
- return parsed ?? undefined;
110
- }
111
78
  }
@@ -1,6 +1,6 @@
1
1
  export type DevRebuildRequest = Readonly<{
2
2
  changedPaths: ReadonlyArray<string>;
3
- shouldRepublishConsumerOutput: boolean;
3
+ configPathOverride?: string;
4
4
  shouldRestartUi: boolean;
5
5
  }>;
6
6
 
@@ -47,7 +47,7 @@ export class DevRebuildQueue {
47
47
  }
48
48
  return {
49
49
  changedPaths: [...new Set([...current.changedPaths, ...next.changedPaths])],
50
- shouldRepublishConsumerOutput: current.shouldRepublishConsumerOutput || next.shouldRepublishConsumerOutput,
50
+ configPathOverride: next.configPathOverride ?? current.configPathOverride,
51
51
  shouldRestartUi: current.shouldRestartUi || next.shouldRestartUi,
52
52
  };
53
53
  }
@@ -1,12 +1,12 @@
1
1
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
2
2
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
3
3
 
4
- import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
5
4
  import { DevHttpProbe } from "./DevHttpProbe";
6
5
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
7
6
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
8
7
  import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
9
8
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
9
+ import { NextHostEdgeSeedLoader } from "./NextHostEdgeSeedLoader";
10
10
  import { WatchRootsResolver } from "./WatchRootsResolver";
11
11
 
12
12
  /**
@@ -19,7 +19,7 @@ export class DevSessionServices {
19
19
  readonly sessionPorts: DevSessionPortsResolver,
20
20
  readonly loopbackPortAllocator: LoopbackPortAllocator,
21
21
  readonly devHttpProbe: DevHttpProbe,
22
- readonly devAuthLoader: DevAuthSettingsLoader,
22
+ readonly nextHostEdgeSeedLoader: NextHostEdgeSeedLoader,
23
23
  readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder,
24
24
  readonly watchRootsResolver: WatchRootsResolver,
25
25
  readonly sourceChangeClassifier: DevSourceChangeClassifier,
@@ -1,21 +1,16 @@
1
1
  import path from "node:path";
2
2
 
3
3
  export class DevSourceChangeClassifier {
4
- private static readonly configFileNames = new Set([
4
+ private static readonly uiConfigFileNames = new Set([
5
5
  "codemation.config.ts",
6
6
  "codemation.config.js",
7
7
  "codemation.config.mjs",
8
8
  ]);
9
-
10
- shouldRepublishConsumerOutput(
11
- args: Readonly<{
12
- changedPaths: ReadonlyArray<string>;
13
- consumerRoot: string;
14
- }>,
15
- ): boolean {
16
- const resolvedConsumerRoot = path.resolve(args.consumerRoot);
17
- return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
18
- }
9
+ private static readonly pluginConfigFileNames = new Set([
10
+ "codemation.plugin.ts",
11
+ "codemation.plugin.js",
12
+ "codemation.plugin.mjs",
13
+ ]);
19
14
 
20
15
  requiresUiRestart(
21
16
  args: Readonly<{
@@ -40,10 +35,15 @@ export class DevSourceChangeClassifier {
40
35
  return false;
41
36
  }
42
37
  const relativePath = path.relative(consumerRoot, resolvedPath);
43
- if (DevSourceChangeClassifier.configFileNames.has(path.basename(relativePath))) {
38
+ const fileName = path.basename(relativePath);
39
+ if (DevSourceChangeClassifier.uiConfigFileNames.has(fileName)) {
44
40
  // Config changes affect auth and branding projections consumed by the packaged Next host.
45
41
  return true;
46
42
  }
43
+ if (DevSourceChangeClassifier.pluginConfigFileNames.has(fileName)) {
44
+ // Plugin sandbox configs also define workflows, so keep them on the cheap runtime-only reload path.
45
+ return false;
46
+ }
47
47
  if (relativePath.startsWith(path.join("src", "workflows"))) {
48
48
  return false;
49
49
  }
@@ -4,13 +4,12 @@ import { CodemationConsumerConfigLoader } from "@codemation/host/server";
4
4
 
5
5
  import type { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
6
6
 
7
- export type DevResolvedAuthSettings = Readonly<{
8
- authConfigJson: string;
7
+ export type NextHostEdgeSeed = Readonly<{
9
8
  authSecret: string;
10
- skipUiAuth: boolean;
9
+ uiAuthEnabled: boolean;
11
10
  }>;
12
11
 
13
- export class DevAuthSettingsLoader {
12
+ export class NextHostEdgeSeedLoader {
14
13
  static readonly defaultDevelopmentAuthSecret = "codemation-dev-auth-secret-not-for-production";
15
14
 
16
15
  constructor(
@@ -25,13 +24,18 @@ export class DevAuthSettingsLoader {
25
24
  return randomUUID();
26
25
  }
27
26
 
28
- async loadForConsumer(consumerRoot: string): Promise<DevResolvedAuthSettings> {
29
- const resolution = await this.configLoader.load({ consumerRoot });
27
+ async loadForConsumer(
28
+ consumerRoot: string,
29
+ options?: Readonly<{ configPathOverride?: string }>,
30
+ ): Promise<NextHostEdgeSeed> {
31
+ const resolution = await this.configLoader.load({
32
+ consumerRoot,
33
+ configPathOverride: options?.configPathOverride,
34
+ });
30
35
  const envForAuthSecret = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(consumerRoot, process.env);
31
36
  return {
32
- authConfigJson: JSON.stringify(resolution.config.auth ?? null),
33
37
  authSecret: this.resolveDevelopmentAuthSecret(envForAuthSecret),
34
- skipUiAuth: resolution.config.auth?.allowUnauthenticatedInDevelopment === true,
38
+ uiAuthEnabled: resolution.config.auth?.allowUnauthenticatedInDevelopment !== true,
35
39
  };
36
40
  }
37
41
 
@@ -40,6 +44,6 @@ export class DevAuthSettingsLoader {
40
44
  if (configuredSecret && configuredSecret.trim().length > 0) {
41
45
  return configuredSecret;
42
46
  }
43
- return DevAuthSettingsLoader.defaultDevelopmentAuthSecret;
47
+ return NextHostEdgeSeedLoader.defaultDevelopmentAuthSecret;
44
48
  }
45
49
  }
@@ -0,0 +1,64 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export type PluginDevConfigBootstrap = Readonly<{
5
+ configPath: string;
6
+ }>;
7
+
8
+ export class PluginDevConfigFactory {
9
+ async prepare(pluginRoot: string): Promise<PluginDevConfigBootstrap> {
10
+ const pluginEntryPath = await this.resolvePluginEntryPath(pluginRoot);
11
+ const configPath = path.resolve(pluginRoot, ".codemation", "plugin-dev", "codemation.config.ts");
12
+ await mkdir(path.dirname(configPath), { recursive: true });
13
+ await writeFile(configPath, this.createConfigSource(configPath, pluginEntryPath), "utf8");
14
+ return {
15
+ configPath,
16
+ };
17
+ }
18
+
19
+ private async resolvePluginEntryPath(pluginRoot: string): Promise<string> {
20
+ const candidates = [
21
+ path.resolve(pluginRoot, "codemation.plugin.ts"),
22
+ path.resolve(pluginRoot, "codemation.plugin.js"),
23
+ path.resolve(pluginRoot, "src", "codemation.plugin.ts"),
24
+ path.resolve(pluginRoot, "src", "codemation.plugin.js"),
25
+ ];
26
+ for (const candidate of candidates) {
27
+ if (await this.exists(candidate)) {
28
+ return candidate;
29
+ }
30
+ }
31
+ throw new Error('Plugin config not found. Expected "codemation.plugin.ts" in the plugin root or "src/".');
32
+ }
33
+
34
+ private createConfigSource(configPath: string, pluginEntryPath: string): string {
35
+ const relativeImportPath = this.toRelativeImportPath(configPath, pluginEntryPath);
36
+ return [
37
+ 'import type { CodemationConfig } from "@codemation/host";',
38
+ `import plugin from ${JSON.stringify(relativeImportPath)};`,
39
+ "",
40
+ "const sandbox = plugin.sandbox ?? {};",
41
+ "const config: CodemationConfig = {",
42
+ " ...sandbox,",
43
+ " plugins: [...(sandbox.plugins ?? []), plugin],",
44
+ "};",
45
+ "",
46
+ "export default config;",
47
+ "",
48
+ ].join("\n");
49
+ }
50
+
51
+ private toRelativeImportPath(fromPath: string, targetPath: string): string {
52
+ const relativePath = path.relative(path.dirname(fromPath), targetPath).replace(/\\/g, "/");
53
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
54
+ }
55
+
56
+ private async exists(filePath: string): Promise<boolean> {
57
+ try {
58
+ await access(filePath);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ }
@@ -1,77 +0,0 @@
1
- import type { CodemationDiscoveredPluginPackage } from "@codemation/host/server";
2
- import { randomUUID } from "node:crypto";
3
- import { mkdir, rename, writeFile } from "node:fs/promises";
4
- import path from "node:path";
5
- import { pathToFileURL } from "node:url";
6
-
7
- import type { ConsumerOutputBuildSnapshot } from "../consumer/ConsumerOutputBuilder";
8
-
9
- export type ConsumerBuildManifest = Readonly<{
10
- buildVersion: string;
11
- consumerRoot: string;
12
- entryPath: string;
13
- manifestPath: string;
14
- pluginEntryPath: string;
15
- workflowSourcePaths: ReadonlyArray<string>;
16
- }>;
17
-
18
- export class ConsumerBuildArtifactsPublisher {
19
- async publish(
20
- snapshot: ConsumerOutputBuildSnapshot,
21
- discoveredPlugins: ReadonlyArray<CodemationDiscoveredPluginPackage>,
22
- ): Promise<ConsumerBuildManifest> {
23
- const pluginEntryPath = await this.writeDiscoveredPluginsOutput(snapshot, discoveredPlugins);
24
- return await this.writeBuildManifest(snapshot, pluginEntryPath);
25
- }
26
-
27
- private async writeDiscoveredPluginsOutput(
28
- snapshot: ConsumerOutputBuildSnapshot,
29
- discoveredPlugins: ReadonlyArray<CodemationDiscoveredPluginPackage>,
30
- ): Promise<string> {
31
- const outputPath = path.resolve(snapshot.emitOutputRoot, "plugins.js");
32
- await mkdir(path.dirname(outputPath), { recursive: true });
33
- const outputLines: string[] = ["const codemationDiscoveredPlugins = [];", ""];
34
- discoveredPlugins.forEach((discoveredPlugin: CodemationDiscoveredPluginPackage, index: number) => {
35
- const pluginFileUrl = pathToFileURL(
36
- path.resolve(discoveredPlugin.packageRoot, discoveredPlugin.manifest.entry),
37
- ).href;
38
- const exportNameAccessor = discoveredPlugin.manifest.exportName
39
- ? `pluginModule${index}[${JSON.stringify(discoveredPlugin.manifest.exportName)}]`
40
- : `pluginModule${index}.default ?? pluginModule${index}.codemationPlugin`;
41
- outputLines.push(`const pluginModule${index} = await import(${JSON.stringify(pluginFileUrl)});`);
42
- outputLines.push(`const pluginValue${index} = ${exportNameAccessor};`);
43
- outputLines.push(`if (pluginValue${index} && typeof pluginValue${index}.register === "function") {`);
44
- outputLines.push(` codemationDiscoveredPlugins.push(pluginValue${index});`);
45
- outputLines.push(
46
- `} else if (typeof pluginValue${index} === "function" && pluginValue${index}.prototype && typeof pluginValue${index}.prototype.register === "function") {`,
47
- );
48
- outputLines.push(` codemationDiscoveredPlugins.push(new pluginValue${index}());`);
49
- outputLines.push("}");
50
- outputLines.push("");
51
- });
52
- outputLines.push("export { codemationDiscoveredPlugins };");
53
- outputLines.push("export default codemationDiscoveredPlugins;");
54
- outputLines.push("");
55
- await writeFile(outputPath, outputLines.join("\n"), "utf8");
56
- return outputPath;
57
- }
58
-
59
- private async writeBuildManifest(
60
- snapshot: ConsumerOutputBuildSnapshot,
61
- pluginEntryPath: string,
62
- ): Promise<ConsumerBuildManifest> {
63
- const manifest: ConsumerBuildManifest = {
64
- buildVersion: snapshot.buildVersion,
65
- consumerRoot: snapshot.consumerRoot,
66
- entryPath: snapshot.outputEntryPath,
67
- manifestPath: snapshot.manifestPath,
68
- pluginEntryPath,
69
- workflowSourcePaths: snapshot.workflowSourcePaths,
70
- };
71
- await mkdir(path.dirname(snapshot.manifestPath), { recursive: true });
72
- const temporaryManifestPath = `${snapshot.manifestPath}.${snapshot.buildVersion}.${randomUUID()}.tmp`;
73
- await writeFile(temporaryManifestPath, JSON.stringify(manifest, null, 2), "utf8");
74
- await rename(temporaryManifestPath, snapshot.manifestPath);
75
- return manifest;
76
- }
77
- }
@@ -1,8 +0,0 @@
1
- import type { ConsumerBuildOptions } from "./consumerBuildOptions.types";
2
- import { ConsumerOutputBuilder } from "./ConsumerOutputBuilder";
3
-
4
- export class ConsumerOutputBuilderLoader {
5
- create(consumerRoot: string, buildOptions: ConsumerBuildOptions): ConsumerOutputBuilder {
6
- return new ConsumerOutputBuilder(consumerRoot, undefined, buildOptions);
7
- }
8
- }
@@ -1,30 +0,0 @@
1
- import type { Logger } from "@codemation/host/next/server";
2
- import { CodemationPluginDiscovery } from "@codemation/host/server";
3
-
4
- import { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
5
- import { ConsumerBuildOptionsParser } from "../build/ConsumerBuildOptionsParser";
6
- import { ConsumerOutputBuilderLoader } from "../consumer/Loader";
7
- import type { CliPaths } from "../path/CliPathResolver";
8
-
9
- /**
10
- * Ensures `.codemation/output/current.json` and transpiled consumer config exist before the Next host boots.
11
- * Without this, `codemation dev` can serve a stale built `codemation.config.js` (e.g. missing whitelabel).
12
- */
13
- export class DevConsumerPublishBootstrap {
14
- constructor(
15
- private readonly cliLogger: Logger,
16
- private readonly pluginDiscovery: CodemationPluginDiscovery,
17
- private readonly artifactsPublisher: ConsumerBuildArtifactsPublisher,
18
- private readonly outputBuilderLoader: ConsumerOutputBuilderLoader,
19
- private readonly buildOptionsParser: ConsumerBuildOptionsParser,
20
- ) {}
21
-
22
- async ensurePublished(paths: CliPaths): Promise<void> {
23
- const buildOptions = this.buildOptionsParser.parse({});
24
- const builder = this.outputBuilderLoader.create(paths.consumerRoot, buildOptions);
25
- const snapshot = await builder.ensureBuilt();
26
- const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
27
- await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
28
- this.cliLogger.debug(`Dev: consumer output published (${snapshot.buildVersion}).`);
29
- }
30
- }