@astrale-os/sdk 0.5.0-beta.68 → 0.5.0-beta.69
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/CHANGELOG.md +14 -0
- package/dist/deployment/adapter/adapter.d.ts +3 -3
- package/dist/deployment/adapter/define.js +3 -3
- package/dist/deployment/adapter/development/context.d.ts +14 -0
- package/dist/deployment/adapter/development/index.d.ts +4 -0
- package/dist/deployment/adapter/{watch/context.d.ts → development/input.d.ts} +3 -5
- package/dist/deployment/adapter/development/placement.d.ts +5 -0
- package/dist/deployment/adapter/development/placement.js +1 -0
- package/dist/deployment/adapter/development/session.d.ts +9 -0
- package/dist/deployment/adapter/development/session.js +1 -0
- package/dist/deployment/adapter/index.d.ts +1 -1
- package/dist/deployment/adapter/prepare/context.d.ts +7 -0
- package/dist/deployment/index.d.ts +1 -1
- package/dist/tooling/cli/arguments.d.ts +0 -1
- package/dist/tooling/cli/arguments.js +41 -13
- package/dist/tooling/cli/bun.d.ts +1 -0
- package/dist/tooling/cli/bun.js +7 -0
- package/dist/tooling/cli/development/develop.d.ts +8 -0
- package/dist/tooling/cli/development/develop.js +284 -0
- package/dist/tooling/cli/development/lifecycle.d.ts +9 -0
- package/dist/tooling/cli/development/lifecycle.js +39 -0
- package/dist/tooling/cli/development/project-lock.d.ts +17 -0
- package/dist/tooling/cli/development/project-lock.js +193 -0
- package/dist/tooling/cli/development/rebuild.d.ts +13 -0
- package/dist/tooling/cli/development/rebuild.js +58 -0
- package/dist/tooling/cli/development/source-watch.d.ts +14 -0
- package/dist/tooling/cli/development/source-watch.js +92 -0
- package/dist/tooling/cli/orchestrate.js +23 -64
- package/dist/tooling/linter/implementations/source/global.js +4 -2
- package/dist/tooling/linter/implementations/source/views.js +23 -0
- package/dist/tooling/linter/policy/generated.js +2 -2
- package/dist/tooling/linter/requirements/registry.js +1 -0
- package/package.json +1 -1
- package/dist/deployment/adapter/watch/handle.d.ts +0 -5
- package/dist/deployment/adapter/watch/index.d.ts +0 -2
- /package/dist/deployment/adapter/{watch → development}/context.js +0 -0
- /package/dist/deployment/adapter/{watch → development}/index.js +0 -0
- /package/dist/deployment/adapter/{watch/handle.js → development/input.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0-beta.69](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.68...sdk-v0.5.0-beta.69) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add coordinated Domain development sessions ([#300](https://github.com/astrale-os/sdk/issues/300)) ([102104f](https://github.com/astrale-os/sdk/commit/102104f1ed55333f5892db23c036578aa0ffd2e1))
|
|
9
|
+
* **tooling:** keep frontend composition application-owned ([#310](https://github.com/astrale-os/sdk/issues/310)) ([31e9bb6](https://github.com/astrale-os/sdk/commit/31e9bb6de256ef9cdbcdb0296abb973086f11ff0))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **adapter-astrale:** preserve explicit Services installation ownership ([#305](https://github.com/astrale-os/sdk/issues/305)) ([1b9dd64](https://github.com/astrale-os/sdk/commit/1b9dd6422e5d80a552ec5a70b5d262b8413dd6bc))
|
|
15
|
+
* **cli:** skip unused secrets files ([3a1c534](https://github.com/astrale-os/sdk/commit/3a1c53439d5172add9c2c724060daaffc0a517bb))
|
|
16
|
+
|
|
3
17
|
## [0.5.0-beta.68](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.67...sdk-v0.5.0-beta.68) (2026-08-28)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { DeployContext, DeployResult } from './deploy/index.js';
|
|
2
|
+
import type { DevelopmentContext, DevelopmentSession } from './development/index.js';
|
|
2
3
|
import type { ArtifactSource, PrepareContext } from './prepare/index.js';
|
|
3
|
-
import type { WatchContext, WatchHandle } from './watch/index.js';
|
|
4
4
|
export interface Adapter<Parameters = unknown> {
|
|
5
5
|
readonly kind: 'adapter';
|
|
6
6
|
readonly name: string;
|
|
7
7
|
readonly version: string;
|
|
8
8
|
parameters(environment: string): Parameters;
|
|
9
9
|
prepare(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
|
|
10
|
-
|
|
10
|
+
develop(parameters: Parameters, context: DevelopmentContext): Promise<DevelopmentSession>;
|
|
11
11
|
deploy(parameters: Parameters, context: DeployContext): Promise<DeployResult>;
|
|
12
12
|
readonly secretsFile?: (parameters: Parameters) => string | undefined;
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@ export interface AdapterInput<Parameters> {
|
|
|
16
16
|
readonly version: string;
|
|
17
17
|
readonly environments: Readonly<Record<string, Parameters>>;
|
|
18
18
|
prepare(parameters: Parameters, context: PrepareContext): Promise<ArtifactSource>;
|
|
19
|
-
|
|
19
|
+
develop(parameters: Parameters, context: DevelopmentContext): Promise<DevelopmentSession>;
|
|
20
20
|
deploy(parameters: Parameters, context: DeployContext): Promise<DeployResult>;
|
|
21
21
|
readonly secretsFile?: (parameters: Parameters) => string | undefined;
|
|
22
22
|
}
|
|
@@ -2,7 +2,7 @@ const admittedAdapters = new WeakSet();
|
|
|
2
2
|
/** Capture one receiver-bound provider adapter and its environment parameters. */
|
|
3
3
|
export function defineAdapter(input) {
|
|
4
4
|
const value = record(input, 'Adapter definition');
|
|
5
|
-
exact(value, ['name', 'version', 'environments', 'prepare', '
|
|
5
|
+
exact(value, ['name', 'version', 'environments', 'prepare', 'develop', 'deploy'], ['secretsFile']);
|
|
6
6
|
const name = stableName(input.name);
|
|
7
7
|
const version = stableName(input.version);
|
|
8
8
|
const environments = record(input.environments, 'Adapter environments');
|
|
@@ -13,7 +13,7 @@ export function defineAdapter(input) {
|
|
|
13
13
|
const names = Object.keys(capturedEnvironments).sort();
|
|
14
14
|
const receiver = Object.freeze({ ...input, environments: capturedEnvironments });
|
|
15
15
|
const prepare = capture(receiver, 'prepare');
|
|
16
|
-
const
|
|
16
|
+
const develop = capture(receiver, 'develop');
|
|
17
17
|
const deploy = capture(receiver, 'deploy');
|
|
18
18
|
const secretsFile = optional(receiver, 'secretsFile');
|
|
19
19
|
const adapter = Object.freeze({
|
|
@@ -27,7 +27,7 @@ export function defineAdapter(input) {
|
|
|
27
27
|
throw new TypeError(`Adapter ${name} has no environment ${environment}; known environments: ${names.length === 0 ? '(none)' : names.join(', ')}.`);
|
|
28
28
|
},
|
|
29
29
|
prepare,
|
|
30
|
-
|
|
30
|
+
develop,
|
|
31
31
|
deploy,
|
|
32
32
|
...(secretsFile === undefined ? {} : { secretsFile }),
|
|
33
33
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DevelopmentInput } from './input.js';
|
|
2
|
+
import type { DevelopmentPlacement } from './placement.js';
|
|
3
|
+
export interface DevelopmentOverrides {
|
|
4
|
+
readonly publicUrl?: string;
|
|
5
|
+
readonly localPort?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Provider-neutral inputs retained for one complete development session. */
|
|
8
|
+
export interface DevelopmentContext extends DevelopmentInput {
|
|
9
|
+
readonly projectDir: string;
|
|
10
|
+
readonly environment: string;
|
|
11
|
+
readonly overrides: DevelopmentOverrides;
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
onPlacement(placement: DevelopmentPlacement): Promise<void>;
|
|
14
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { PreparedArtifact } from '../prepare/index.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
readonly environment: string;
|
|
5
|
-
readonly secrets: Readonly<Record<string, string>>;
|
|
2
|
+
/** One admitted application artifact and its declared provider secrets. */
|
|
3
|
+
export interface DevelopmentInput {
|
|
6
4
|
readonly artifact: PreparedArtifact;
|
|
7
|
-
|
|
5
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
8
6
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DevelopmentInput } from './input.js';
|
|
2
|
+
import type { DevelopmentPlacement } from './placement.js';
|
|
3
|
+
/** Lifecycle capability for one ready development placement. */
|
|
4
|
+
export interface DevelopmentSession {
|
|
5
|
+
current(): DevelopmentPlacement;
|
|
6
|
+
update(input: DevelopmentInput): Promise<void>;
|
|
7
|
+
readonly finished: Promise<void>;
|
|
8
|
+
stop(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Adapter, AdapterInput } from './adapter.js';
|
|
2
2
|
export { defineAdapter, isAdapter } from './define.js';
|
|
3
3
|
export type * from './prepare/index.js';
|
|
4
|
-
export type * from './
|
|
4
|
+
export type * from './development/index.js';
|
|
5
5
|
export type * from './deploy/index.js';
|
|
@@ -3,6 +3,13 @@ import type { ResolvedRuntimeReference } from '../../runtime/index.js';
|
|
|
3
3
|
export interface PrepareContext<BuildValue extends Build = Build> {
|
|
4
4
|
readonly projectDir: string;
|
|
5
5
|
readonly environment: string;
|
|
6
|
+
/**
|
|
7
|
+
* Preparation purpose. Development may omit immutable frontend output because
|
|
8
|
+
* the live provider serves the declared frontend source directly; release
|
|
9
|
+
* preparation must remain complete. Omitted by older custom callers means
|
|
10
|
+
* release for compatibility.
|
|
11
|
+
*/
|
|
12
|
+
readonly mode?: 'development' | 'release';
|
|
6
13
|
readonly build: BuildValue;
|
|
7
14
|
readonly runtime: ResolvedRuntimeReference;
|
|
8
15
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defineAdapter, isAdapter } from './adapter/index.js';
|
|
2
|
-
export type { Adapter, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, DeployContext, DeploymentPhase, DeploymentProgress, DeployResult, FailedDeployResult, MutationKnowledge, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult,
|
|
2
|
+
export type { Adapter, AdapterInput, AddressingPlan, ArtifactFile, ArtifactSource, DeployContext, DevelopmentContext, DevelopmentInput, DevelopmentOverrides, DevelopmentPlacement, DevelopmentSession, DeploymentPhase, DeploymentProgress, DeployResult, FailedDeployResult, MutationKnowledge, PreparedArtifact, PreparedArtifactFile, PrepareContext, ReadyDeployResult, } from './adapter/index.js';
|
|
3
3
|
export { compile, isBuild } from './build/index.js';
|
|
4
4
|
export type { Build, BuildBundle, BuildSchema } from './build/index.js';
|
|
5
5
|
export { deploy, isDeployment } from './deploy.js';
|
|
@@ -2,7 +2,6 @@ import type { LintReportFormat } from '../linter/index.js';
|
|
|
2
2
|
export interface ParsedArgs {
|
|
3
3
|
readonly command: 'dev' | 'build' | 'deploy' | 'lint' | 'package';
|
|
4
4
|
readonly env: string;
|
|
5
|
-
readonly watch: boolean;
|
|
6
5
|
readonly fix?: boolean;
|
|
7
6
|
readonly format?: LintReportFormat;
|
|
8
7
|
readonly port?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
1
2
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
2
3
|
export function parseArgs(argv) {
|
|
3
4
|
const [command, ...rest] = argv;
|
|
4
|
-
const watch = rest.includes('--watch');
|
|
5
5
|
const fix = rest.includes('--fix');
|
|
6
6
|
let port;
|
|
7
7
|
let host;
|
|
@@ -13,13 +13,13 @@ export function parseArgs(argv) {
|
|
|
13
13
|
if (argument === '--port') {
|
|
14
14
|
const value = rest[++index];
|
|
15
15
|
port = Number(value);
|
|
16
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
16
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
17
17
|
throw new Error(`--port needs a port number, got "${value}"`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
else if (argument.startsWith('--port=')) {
|
|
21
21
|
port = Number(argument.slice('--port='.length));
|
|
22
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
22
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
23
23
|
throw new Error(`--port needs a port number, got "${argument}"`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -41,7 +41,7 @@ export function parseArgs(argv) {
|
|
|
41
41
|
else if (argument.startsWith('--environment=')) {
|
|
42
42
|
environment = requiredValue('--environment', argument.slice('--environment='.length));
|
|
43
43
|
}
|
|
44
|
-
else if (argument === '--
|
|
44
|
+
else if (argument === '--fix') {
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
47
|
else if (argument.startsWith('-')) {
|
|
@@ -56,9 +56,6 @@ export function parseArgs(argv) {
|
|
|
56
56
|
if (format !== undefined && command !== 'lint') {
|
|
57
57
|
throw new Error('`--format` is only valid for `lint`.');
|
|
58
58
|
}
|
|
59
|
-
if (watch && !['dev', 'deploy'].includes(command ?? '')) {
|
|
60
|
-
throw new Error('`--watch` is only valid for the dev / deploy commands.');
|
|
61
|
-
}
|
|
62
59
|
if ((port !== undefined || host !== undefined) && command !== 'dev') {
|
|
63
60
|
throw new Error('`--port` and `--host` are only valid for `dev`.');
|
|
64
61
|
}
|
|
@@ -76,7 +73,6 @@ export function parseArgs(argv) {
|
|
|
76
73
|
return {
|
|
77
74
|
command: 'dev',
|
|
78
75
|
env: environment ?? positionals[0] ?? 'dev',
|
|
79
|
-
watch: true,
|
|
80
76
|
...(port !== undefined ? { port } : {}),
|
|
81
77
|
...(host !== undefined ? { host } : {}),
|
|
82
78
|
};
|
|
@@ -89,24 +85,22 @@ export function parseArgs(argv) {
|
|
|
89
85
|
return {
|
|
90
86
|
command: 'deploy',
|
|
91
87
|
env,
|
|
92
|
-
watch,
|
|
93
88
|
};
|
|
94
89
|
}
|
|
95
90
|
case 'build':
|
|
96
91
|
if (positionals.length > 0)
|
|
97
92
|
throw new Error('`build` does not accept positional arguments.');
|
|
98
|
-
return { command: 'build', env: 'dev'
|
|
93
|
+
return { command: 'build', env: 'dev' };
|
|
99
94
|
case 'package':
|
|
100
95
|
if (positionals.length > 0)
|
|
101
96
|
throw new Error('`package` does not accept positional arguments.');
|
|
102
|
-
return { command: 'package', env: 'dev'
|
|
97
|
+
return { command: 'package', env: 'dev' };
|
|
103
98
|
case 'lint':
|
|
104
99
|
if (positionals.length > 0)
|
|
105
100
|
throw new Error('`lint` does not accept positional arguments.');
|
|
106
101
|
return {
|
|
107
102
|
command: 'lint',
|
|
108
103
|
env: 'dev',
|
|
109
|
-
watch: false,
|
|
110
104
|
...(fix ? { fix: true } : {}),
|
|
111
105
|
...(format ? { format } : {}),
|
|
112
106
|
};
|
|
@@ -119,12 +113,46 @@ function normalizeHost(input) {
|
|
|
119
113
|
throw new Error(`--host needs a public URL, got "${input ?? ''}"`);
|
|
120
114
|
}
|
|
121
115
|
try {
|
|
122
|
-
|
|
116
|
+
const url = new URL(input);
|
|
117
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') ||
|
|
118
|
+
url.username !== '' ||
|
|
119
|
+
url.password !== '' ||
|
|
120
|
+
url.pathname !== '/' ||
|
|
121
|
+
url.search !== '' ||
|
|
122
|
+
url.hash !== '' ||
|
|
123
|
+
privateHostname(url.hostname))
|
|
124
|
+
throw new Error('invalid');
|
|
125
|
+
return url.origin;
|
|
123
126
|
}
|
|
124
127
|
catch {
|
|
125
128
|
throw new Error(`--host needs a valid URL, got "${input}"`);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
131
|
+
function privateHostname(input) {
|
|
132
|
+
const host = input
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/^\[|\]$/gu, '')
|
|
135
|
+
.replace(/\.$/u, '');
|
|
136
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
137
|
+
return true;
|
|
138
|
+
if (isIP(host) === 4) {
|
|
139
|
+
const parts = host.split('.').map(Number);
|
|
140
|
+
return (parts[0] === 0 ||
|
|
141
|
+
parts[0] === 10 ||
|
|
142
|
+
parts[0] === 127 ||
|
|
143
|
+
(parts[0] === 169 && parts[1] === 254) ||
|
|
144
|
+
(parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31) ||
|
|
145
|
+
(parts[0] === 192 && parts[1] === 168));
|
|
146
|
+
}
|
|
147
|
+
if (isIP(host) !== 6)
|
|
148
|
+
return false;
|
|
149
|
+
return (host === '::' ||
|
|
150
|
+
host === '::1' ||
|
|
151
|
+
host.startsWith('fc') ||
|
|
152
|
+
host.startsWith('fd') ||
|
|
153
|
+
/^fe[89ab]/u.test(host) ||
|
|
154
|
+
/^::ffff:(?:0|10|127|169\.254|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./u.test(host));
|
|
155
|
+
}
|
|
128
156
|
function lintFormat(input) {
|
|
129
157
|
if (input !== 'stylish' && input !== 'json') {
|
|
130
158
|
throw new Error(`--format must be "stylish" or "json", got "${input ?? ''}"`);
|
|
@@ -10,5 +10,6 @@ export type BunOutcome = {
|
|
|
10
10
|
};
|
|
11
11
|
/** Re-enter the published executable under Bun only when authored TypeScript must be imported. */
|
|
12
12
|
export declare function runWithBun(executable: string, argv: readonly string[]): Promise<BunOutcome>;
|
|
13
|
+
export declare function isolateDevelopmentProcessGroup(argv: readonly string[], platform?: NodeJS.Platform): boolean;
|
|
13
14
|
/** Prefer the generated project's exact Bun package binary over an ambient executable or shim. */
|
|
14
15
|
export declare function resolveBunExecutable(cwd: string): string;
|
package/dist/tooling/cli/bun.js
CHANGED
|
@@ -14,6 +14,10 @@ export function runWithBun(executable, argv) {
|
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
15
15
|
const child = spawn(resolveBunExecutable(process.cwd()), [executable, ...argv], {
|
|
16
16
|
stdio: 'inherit',
|
|
17
|
+
// Keep a long-lived development runtime outside the launcher's terminal
|
|
18
|
+
// process group. The launcher receives Ctrl-C, forwards it exactly once,
|
|
19
|
+
// and remains alive until Bun finishes provider and Kernel cleanup.
|
|
20
|
+
detached: isolateDevelopmentProcessGroup(argv),
|
|
17
21
|
});
|
|
18
22
|
const handlers = FORWARDED_SIGNALS.map((signal) => [signal, () => child.kill(signal)]);
|
|
19
23
|
for (const [signal, handler] of handlers)
|
|
@@ -40,6 +44,9 @@ export function runWithBun(executable, argv) {
|
|
|
40
44
|
});
|
|
41
45
|
});
|
|
42
46
|
}
|
|
47
|
+
export function isolateDevelopmentProcessGroup(argv, platform = process.platform) {
|
|
48
|
+
return platform !== 'win32' && argv[0] === 'dev';
|
|
49
|
+
}
|
|
43
50
|
/** Prefer the generated project's exact Bun package binary over an ambient executable or shim. */
|
|
44
51
|
export function resolveBunExecutable(cwd) {
|
|
45
52
|
const projectRequire = createRequire(join(cwd, 'package.json'));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DevelopmentOverrides } from '../../../deployment/adapter/index.js';
|
|
2
|
+
/** Run one complete provider-neutral development session until interruption. */
|
|
3
|
+
export declare function develop(input: {
|
|
4
|
+
readonly configPath: string;
|
|
5
|
+
readonly projectDir: string;
|
|
6
|
+
readonly environment: string;
|
|
7
|
+
readonly overrides: DevelopmentOverrides;
|
|
8
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { admitRuntime, compile, isDeployment, resolveRuntime, verifyPreflight, } from '../../../deployment/index.js';
|
|
6
|
+
import { loadDeclaredSecrets } from '../dotenv.js';
|
|
7
|
+
import { error, info } from '../log.js';
|
|
8
|
+
import { DevelopmentLifecycle } from './lifecycle.js';
|
|
9
|
+
import { acquireProjectLock } from './project-lock.js';
|
|
10
|
+
import { LatestRebuild } from './rebuild.js';
|
|
11
|
+
import { watchSources } from './source-watch.js';
|
|
12
|
+
/** Run one complete provider-neutral development session until interruption. */
|
|
13
|
+
export async function develop(input) {
|
|
14
|
+
const lifecycle = new DevelopmentLifecycle();
|
|
15
|
+
let lock;
|
|
16
|
+
let session;
|
|
17
|
+
let sourceWatch;
|
|
18
|
+
let rebuild;
|
|
19
|
+
let failure;
|
|
20
|
+
let failed = false;
|
|
21
|
+
let watcherFailure;
|
|
22
|
+
try {
|
|
23
|
+
lock = await acquireProjectLock({
|
|
24
|
+
projectRoot: input.projectDir,
|
|
25
|
+
environment: input.environment,
|
|
26
|
+
});
|
|
27
|
+
const runtimePath = await discoverRuntimePath(input.configPath);
|
|
28
|
+
const builder = new ProjectBuilder({ ...input, runtimePath });
|
|
29
|
+
let current = await builder.prepare(lifecycle.signal);
|
|
30
|
+
const adapter = current.adapter;
|
|
31
|
+
const parameters = current.parameters;
|
|
32
|
+
let restartReported = false;
|
|
33
|
+
info(`Domain: ${current.artifact.build.schema.compiled.root.origin}`);
|
|
34
|
+
session = await adapter.develop(parameters, {
|
|
35
|
+
projectDir: input.projectDir,
|
|
36
|
+
environment: input.environment,
|
|
37
|
+
artifact: current.artifact,
|
|
38
|
+
secrets: current.secrets,
|
|
39
|
+
overrides: input.overrides,
|
|
40
|
+
signal: lifecycle.signal,
|
|
41
|
+
async onPlacement(placement) {
|
|
42
|
+
info(`Public: ${placement.releaseUrl}`);
|
|
43
|
+
if (placement.localUrl !== undefined)
|
|
44
|
+
info(`Runtime: ${placement.localUrl}`);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
rebuild = new LatestRebuild({
|
|
48
|
+
prepare: (_ignored, signal) => builder.prepare(signal),
|
|
49
|
+
async commit(candidate) {
|
|
50
|
+
if (candidate.adapter.name !== adapter.name ||
|
|
51
|
+
candidate.adapter.version !== adapter.version ||
|
|
52
|
+
stable(candidate.parameters) !== stable(parameters)) {
|
|
53
|
+
restartReported = true;
|
|
54
|
+
throw new Error('Adapter configuration changed; restart pnpm dev to apply it.');
|
|
55
|
+
}
|
|
56
|
+
const previous = current;
|
|
57
|
+
current = candidate;
|
|
58
|
+
try {
|
|
59
|
+
await session.update({ artifact: candidate.artifact, secrets: candidate.secrets });
|
|
60
|
+
sourceWatch?.update(candidate.sources);
|
|
61
|
+
info(`Updated ${candidate.artifact.build.schema.compiled.root.revision}.`);
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
current = previous;
|
|
65
|
+
throw cause;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
failed(cause) {
|
|
69
|
+
error(cause instanceof Error ? cause.message : String(cause));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
sourceWatch = watchSources({
|
|
73
|
+
projectDir: input.projectDir,
|
|
74
|
+
configPath: input.configPath,
|
|
75
|
+
sources: current.sources,
|
|
76
|
+
onInvalidate: () => rebuild.invalidate(),
|
|
77
|
+
onRestartRequired: () => {
|
|
78
|
+
if (restartReported)
|
|
79
|
+
return;
|
|
80
|
+
restartReported = true;
|
|
81
|
+
error('Adapter configuration changed; restart pnpm dev to apply it.');
|
|
82
|
+
},
|
|
83
|
+
onError: (cause) => {
|
|
84
|
+
watcherFailure = cause;
|
|
85
|
+
lifecycle.stop(cause);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const placement = session.current();
|
|
89
|
+
info(`Ready: watching for changes (${placement.releaseUrl}).`);
|
|
90
|
+
const completed = await Promise.race([
|
|
91
|
+
session.finished.then(() => 'session'),
|
|
92
|
+
lifecycle.stopped.then(() => 'signal'),
|
|
93
|
+
]);
|
|
94
|
+
if (completed === 'session' && !lifecycle.signal.aborted) {
|
|
95
|
+
throw new Error('Development provider stopped unexpectedly.');
|
|
96
|
+
}
|
|
97
|
+
if (watcherFailure !== undefined)
|
|
98
|
+
throw watcherFailure;
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
failed = true;
|
|
102
|
+
failure = cause;
|
|
103
|
+
}
|
|
104
|
+
lifecycle.stop(failure);
|
|
105
|
+
const cleanup = [];
|
|
106
|
+
for (const operation of [
|
|
107
|
+
() => sourceWatch?.stop(),
|
|
108
|
+
() => rebuild?.stop(failure),
|
|
109
|
+
() => session?.stop(),
|
|
110
|
+
() => rm(join(input.projectDir, '.astrale', 'development', input.environment, 'source'), {
|
|
111
|
+
recursive: true,
|
|
112
|
+
force: true,
|
|
113
|
+
}),
|
|
114
|
+
() => lock?.release(),
|
|
115
|
+
]) {
|
|
116
|
+
try {
|
|
117
|
+
await operation();
|
|
118
|
+
}
|
|
119
|
+
catch (cause) {
|
|
120
|
+
cleanup.push(cause);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const directory of [
|
|
124
|
+
join(input.projectDir, '.astrale', 'development', input.environment),
|
|
125
|
+
join(input.projectDir, '.astrale', 'development'),
|
|
126
|
+
]) {
|
|
127
|
+
try {
|
|
128
|
+
await rmdir(directory);
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
const code = cause.code;
|
|
132
|
+
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST')
|
|
133
|
+
cleanup.push(cause);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
lifecycle.dispose();
|
|
137
|
+
if (failed) {
|
|
138
|
+
if (cleanup.length > 0)
|
|
139
|
+
error(new AggregateError(cleanup, 'Development cleanup failed.').message);
|
|
140
|
+
throw failure;
|
|
141
|
+
}
|
|
142
|
+
if (cleanup.length > 0)
|
|
143
|
+
throw new AggregateError(cleanup, 'Development cleanup failed.');
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
class ProjectBuilder {
|
|
147
|
+
#configPath;
|
|
148
|
+
#projectDir;
|
|
149
|
+
#environment;
|
|
150
|
+
#runtimePath;
|
|
151
|
+
#generation = 0;
|
|
152
|
+
constructor(input) {
|
|
153
|
+
this.#configPath = input.configPath;
|
|
154
|
+
this.#projectDir = input.projectDir;
|
|
155
|
+
this.#environment = input.environment;
|
|
156
|
+
this.#runtimePath = input.runtimePath;
|
|
157
|
+
}
|
|
158
|
+
async prepare(signal) {
|
|
159
|
+
throwIfAborted(signal);
|
|
160
|
+
const directory = join(this.#projectDir, '.astrale', 'development', this.#environment, 'source', String(++this.#generation));
|
|
161
|
+
await mkdir(directory, { recursive: true });
|
|
162
|
+
const bridge = join(directory, 'bridge.ts');
|
|
163
|
+
const configImport = moduleSpecifier(dirname(bridge), this.#configPath);
|
|
164
|
+
const runtimeImport = moduleSpecifier(dirname(bridge), resolve(dirname(this.#configPath), this.#runtimePath));
|
|
165
|
+
await writeFile(bridge, `export { default as deployment } from ${JSON.stringify(configImport)}\n` +
|
|
166
|
+
`export { default as runtime } from ${JSON.stringify(runtimeImport)}\n`);
|
|
167
|
+
try {
|
|
168
|
+
const built = await bun().build({
|
|
169
|
+
entrypoints: [bridge],
|
|
170
|
+
outdir: directory,
|
|
171
|
+
naming: 'application.mjs',
|
|
172
|
+
target: 'bun',
|
|
173
|
+
format: 'esm',
|
|
174
|
+
packages: 'external',
|
|
175
|
+
splitting: false,
|
|
176
|
+
sourcemap: 'inline',
|
|
177
|
+
metafile: true,
|
|
178
|
+
});
|
|
179
|
+
throwIfAborted(signal);
|
|
180
|
+
const output = built.outputs.find((candidate) => candidate.path.endsWith('application.mjs'));
|
|
181
|
+
if (output === undefined)
|
|
182
|
+
throw new Error('Authored Application rebuild produced no module.');
|
|
183
|
+
const loaded = (await import(`${pathToFileURL(output.path).href}?development=${this.#generation}`));
|
|
184
|
+
if (Object.keys(loaded).some((key) => key !== 'deployment' && key !== 'runtime')) {
|
|
185
|
+
throw new TypeError('Development bridge produced unexpected exports.');
|
|
186
|
+
}
|
|
187
|
+
const deployment = loaded.deployment;
|
|
188
|
+
if (!isDeployment(deployment)) {
|
|
189
|
+
throw new TypeError('astrale.config.ts default export must be created by deploy().');
|
|
190
|
+
}
|
|
191
|
+
const admitted = deployment;
|
|
192
|
+
const resolved = await resolveRuntime(admitted.entrypoint, async (path) => {
|
|
193
|
+
if (path !== this.#runtimePath) {
|
|
194
|
+
throw new Error('Runtime entrypoint changed; restart pnpm dev to apply it.');
|
|
195
|
+
}
|
|
196
|
+
return Object.freeze({ default: loaded.runtime });
|
|
197
|
+
});
|
|
198
|
+
const build = compile(admitted.application);
|
|
199
|
+
admitRuntime({ runtime: build.runtime }, resolved);
|
|
200
|
+
const adapter = admitted.adapter;
|
|
201
|
+
const parameters = adapter.parameters(this.#environment);
|
|
202
|
+
const source = await adapter.prepare(parameters, {
|
|
203
|
+
projectDir: this.#projectDir,
|
|
204
|
+
environment: this.#environment,
|
|
205
|
+
mode: 'development',
|
|
206
|
+
build,
|
|
207
|
+
runtime: resolved,
|
|
208
|
+
});
|
|
209
|
+
const artifact = verifyPreflight({ build, runtime: resolved, source });
|
|
210
|
+
const declared = secretsFor(adapter, parameters, this.#projectDir, resolved.runtime);
|
|
211
|
+
const sources = new Set();
|
|
212
|
+
for (const path of Object.keys(built.metafile?.inputs ?? {})) {
|
|
213
|
+
const absolute = isAbsolute(path) ? path : resolve(this.#projectDir, path);
|
|
214
|
+
if (!absolute.startsWith(`${resolve(this.#projectDir)}${sep}`))
|
|
215
|
+
continue;
|
|
216
|
+
if (absolute.includes(`${sep}.astrale${sep}`))
|
|
217
|
+
continue;
|
|
218
|
+
sources.add(absolute);
|
|
219
|
+
}
|
|
220
|
+
if (declared.path !== undefined)
|
|
221
|
+
sources.add(declared.path);
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
adapter,
|
|
224
|
+
parameters,
|
|
225
|
+
artifact,
|
|
226
|
+
secrets: declared.values,
|
|
227
|
+
sources,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await rm(directory, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function discoverRuntimePath(configPath) {
|
|
236
|
+
const loaded = (await import(`${pathToFileURL(configPath).href}?discover=${Date.now()}`));
|
|
237
|
+
if (Object.keys(loaded).length !== 1 || !Object.hasOwn(loaded, 'default')) {
|
|
238
|
+
throw new TypeError('astrale.config.ts must export exactly one default Deployment.');
|
|
239
|
+
}
|
|
240
|
+
if (!isDeployment(loaded.default)) {
|
|
241
|
+
throw new TypeError('Default export must be created by deploy().');
|
|
242
|
+
}
|
|
243
|
+
return loaded.default.entrypoint.path;
|
|
244
|
+
}
|
|
245
|
+
function secretsFor(adapter, parameters, projectDir, runtime) {
|
|
246
|
+
const file = adapter.secretsFile?.(parameters);
|
|
247
|
+
if (file === undefined)
|
|
248
|
+
return { values: Object.freeze({}) };
|
|
249
|
+
const path = isAbsolute(file) ? file : join(projectDir, file);
|
|
250
|
+
// An Integration-free Runtime has no Providers and therefore no author-owned
|
|
251
|
+
// environment secrets. Adapter-owned secrets, such as the signing identity,
|
|
252
|
+
// are resolved separately by the adapter and must not make this file required.
|
|
253
|
+
if (Object.keys(runtime.integrations).length === 0 && !existsSync(path)) {
|
|
254
|
+
return { values: Object.freeze({}) };
|
|
255
|
+
}
|
|
256
|
+
return { values: Object.freeze(loadDeclaredSecrets(path, file)), path };
|
|
257
|
+
}
|
|
258
|
+
function bun() {
|
|
259
|
+
const value = Reflect.get(globalThis, 'Bun');
|
|
260
|
+
if (value === undefined || typeof value.build !== 'function') {
|
|
261
|
+
throw new TypeError('Domain development requires the Bun runtime.');
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
function moduleSpecifier(from, target) {
|
|
266
|
+
const path = relative(from, target).split(sep).join('/');
|
|
267
|
+
return path.startsWith('.') ? path : `./${path}`;
|
|
268
|
+
}
|
|
269
|
+
function stable(input) {
|
|
270
|
+
if (input === null || typeof input !== 'object')
|
|
271
|
+
return JSON.stringify(input);
|
|
272
|
+
if (Array.isArray(input))
|
|
273
|
+
return `[${input.map(stable).join(',')}]`;
|
|
274
|
+
const record = input;
|
|
275
|
+
return `{${Object.keys(record)
|
|
276
|
+
.sort()
|
|
277
|
+
.map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
|
|
278
|
+
.join(',')}}`;
|
|
279
|
+
}
|
|
280
|
+
function throwIfAborted(signal) {
|
|
281
|
+
if (signal.aborted) {
|
|
282
|
+
throw signal.reason instanceof Error ? signal.reason : new Error('Development interrupted.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Own process signals for exactly one foreground development command. */
|
|
2
|
+
export declare class DevelopmentLifecycle {
|
|
3
|
+
#private;
|
|
4
|
+
constructor();
|
|
5
|
+
get signal(): AbortSignal;
|
|
6
|
+
get stopped(): Promise<void>;
|
|
7
|
+
stop(reason?: unknown): void;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|