@astrale-os/sdk 0.5.0-beta.43 → 0.5.0-beta.44

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.
@@ -1,4 +1,4 @@
1
- /** Canonical Schema authoring, compilation, loading, and resolved Domain surface. */
1
+ import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
2
2
  export * from '@astrale-os/kernel-dsl/v1';
3
3
  export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
4
4
  export type { CallablePolicyBuilder, CallablePolicyContext, CallableProjection, AuthoredMethodProjection, AuthoredPropertyProjection, ClassIdentityProjection, ClassOwnerProjection, CoreEdge, CoreEndpoint, CoreNode, DefinedSchema, DirectedEdgeClass, ClassReferenceProjection, ClassProjection, FunctionProjection, EdgeClass, Function, Method, MethodProjection, Lazy, NodeClass, Policy, Property, PropertyProjection, ResolvedDomainOfInput, View, } from '@astrale-os/kernel-dsl/v1/builder';
@@ -6,5 +6,11 @@ export type { Array, Object, Value } from '@astrale-os/kernel-dsl/value';
6
6
  export type { ClassKind, DirectedEdgeContract, EdgeClassPolicies, NodeClassDefinition, PolicyCheckObject, PropertyFacet, UndirectedEdgeContract, } from '@astrale-os/kernel-dsl/v1/language';
7
7
  export type { DomainSchema } from '@astrale-os/kernel-dsl/v1/schema';
8
8
  /** Kernel-owned Schema values used by authored product Schemas and graph operations. */
9
- export { AuthorityOperationOrder, AuthorityOperations, AuthorityRelations, K, KernelRootFunctions, KernelSchema, OperationCore, operationOf, } from '@astrale-os/kernel-core/schema';
9
+ export { AuthorityOperationOrder, AuthorityOperations, AuthorityRelations, K, KernelRootFunctions, OperationCore, operationOf, } from '@astrale-os/kernel-core/schema';
10
10
  export type { AuthorityOperation, AuthorityRelation, Kernel } from '@astrale-os/kernel-core/schema';
11
+ /**
12
+ * Re-admit the Kernel Schema into the SDK's DSL instance. Package-manager trees may retain a
13
+ * different DSL instance below Kernel Core; acceptance context is intentionally process-local.
14
+ */
15
+ export declare const KernelSchema: typeof CoreKernelSchema;
16
+ export type KernelSchema = typeof CoreKernelSchema;
@@ -1,5 +1,12 @@
1
+ import { KernelSchema as CoreKernelSchema } from '@astrale-os/kernel-core/schema';
1
2
  /** Canonical Schema authoring, compilation, loading, and resolved Domain surface. */
3
+ import { schema as language } from '@astrale-os/kernel-dsl/v1';
2
4
  export * from '@astrale-os/kernel-dsl/v1';
3
5
  export { patterns } from '@astrale-os/kernel-dsl/v1/addressing';
4
6
  /** Kernel-owned Schema values used by authored product Schemas and graph operations. */
5
- export { AuthorityOperationOrder, AuthorityOperations, AuthorityRelations, K, KernelRootFunctions, KernelSchema, OperationCore, operationOf, } from '@astrale-os/kernel-core/schema';
7
+ export { AuthorityOperationOrder, AuthorityOperations, AuthorityRelations, K, KernelRootFunctions, OperationCore, operationOf, } from '@astrale-os/kernel-core/schema';
8
+ /**
9
+ * Re-admit the Kernel Schema into the SDK's DSL instance. Package-manager trees may retain a
10
+ * different DSL instance below Kernel Core; acceptance context is intentionally process-local.
11
+ */
12
+ export const KernelSchema = language.accept(CoreKernelSchema);
@@ -66,7 +66,7 @@ function moduleSpecifier(node) {
66
66
  return node.argument.literal.text;
67
67
  }
68
68
  if (ts.isCallExpression(node) &&
69
- node.arguments.length === 1 &&
69
+ node.arguments.length > 0 &&
70
70
  ts.isStringLiteral(node.arguments[0]) &&
71
71
  (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
72
72
  (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
@@ -1,8 +1,10 @@
1
1
  export interface DomainPackageCompilation {
2
2
  readonly root: string;
3
+ readonly typeClosure: readonly DomainPackageTypeClosure[];
3
4
  }
4
- export interface DomainPackageCompilationInput {
5
- readonly declarations: readonly string[];
5
+ export interface DomainPackageTypeClosure {
6
+ readonly source: string;
7
+ readonly target: string;
6
8
  }
7
- /** Compile one Domain's public runtime package from its dedicated package config. */
8
- export declare function compileDomainPackage(projectDir: string, input: DomainPackageCompilationInput): DomainPackageCompilation;
9
+ /** Compile one Domain's admitted Schema contract with SDK-owned emit policy. */
10
+ export declare function compileDomainPackage(projectDir: string): DomainPackageCompilation;
@@ -1,70 +1,55 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import ts from 'typescript';
6
- /** Compile one Domain's public runtime package from its dedicated package config. */
7
- export function compileDomainPackage(projectDir, input) {
6
+ const entrypoint = 'schema/index.ts';
7
+ /** Compile one Domain's admitted Schema contract with SDK-owned emit policy. */
8
+ export function compileDomainPackage(projectDir) {
8
9
  const project = resolve(projectDir);
9
- const configuration = 'tsconfig.package.json';
10
- const configurationPath = resolve(project, configuration);
10
+ const root = resolve(project, 'dist');
11
+ rmSync(root, { recursive: true, force: true });
12
+ const source = resolve(project, entrypoint);
13
+ if (!existsSync(source))
14
+ throw new TypeError(`Domain contract entrypoint is absent: ${entrypoint}.`);
15
+ const configurationPath = resolve(project, 'tsconfig.json');
11
16
  const loaded = ts.readConfigFile(configurationPath, ts.sys.readFile);
12
17
  if (loaded.error !== undefined)
13
18
  failDiagnostics([loaded.error], project);
14
19
  const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, project, undefined, configurationPath);
15
- const root = parsed.options.outDir && resolve(parsed.options.outDir);
16
- if (root !== undefined)
17
- confineOutput(project, root);
18
20
  if (errors(parsed.errors).length > 0)
19
21
  failDiagnostics(parsed.errors, project);
20
- if (root === undefined)
21
- throw new TypeError(`${configuration} must declare compilerOptions.outDir.`);
22
- if (parsed.options.noEmit || !parsed.options.declaration || parsed.options.emitDeclarationOnly) {
23
- throw new TypeError(`${configuration} must emit both JavaScript and declarations.`);
24
- }
25
- if (parsed.options.declarationDir !== undefined &&
26
- resolve(parsed.options.declarationDir) !== root) {
27
- throw new TypeError(`${configuration} must emit JavaScript and declarations into one outDir.`);
28
- }
29
22
  if (parsed.fileNames.length === 0)
30
- throw new TypeError(`${configuration} compiles no source files.`);
31
- if (root !== resolve(project, 'dist')) {
32
- throw new TypeError('Domain package output must be the project dist directory.');
33
- }
34
- const declarationFiles = input.declarations.map((entrypoint) => declarationSource(project, root, parsed.options.rootDir, entrypoint));
23
+ throw new TypeError('tsconfig.json compiles no source files.');
35
24
  const temporary = mkdtempSync(join(project, '.astrale-package-'));
36
- const declarationConfiguration = resolve(temporary, 'tsconfig.json');
37
- writeFileSync(declarationConfiguration, JSON.stringify({
25
+ const packageConfiguration = resolve(temporary, 'tsconfig.json');
26
+ writeFileSync(packageConfiguration, JSON.stringify({
38
27
  extends: configurationPath,
39
28
  compilerOptions: {
29
+ rootDir: project,
30
+ outDir: root,
40
31
  noEmit: false,
41
32
  declaration: true,
42
33
  declarationMap: false,
43
34
  sourceMap: false,
44
- emitDeclarationOnly: true,
35
+ inlineSourceMap: false,
36
+ inlineSources: false,
37
+ allowImportingTsExtensions: false,
38
+ emitDeclarationOnly: false,
39
+ noEmitOnError: true,
40
+ composite: false,
41
+ incremental: false,
45
42
  },
46
- files: declarationFiles,
43
+ files: [source],
47
44
  include: [],
45
+ exclude: [],
48
46
  }));
49
47
  try {
50
- runCompiler(project, ['-p', resolve(project, 'tsconfig.json'), '--noEmit']);
48
+ const typeClosure = admitSchemaClosure(project, packageConfiguration);
51
49
  runCompiler(project, ['-p', configurationPath, '--noEmit']);
52
- rmSync(root, { recursive: true, force: true });
53
- runCompiler(project, [
54
- '-p',
55
- configurationPath,
56
- '--noEmit',
57
- 'false',
58
- '--declaration',
59
- 'false',
60
- '--declarationMap',
61
- 'false',
62
- '--sourceMap',
63
- 'false',
64
- '--emitDeclarationOnly',
65
- 'false',
66
- ]);
67
- runCompiler(project, ['-p', declarationConfiguration]);
50
+ runCompiler(project, ['-p', packageConfiguration]);
51
+ relocateTypeClosure(root, typeClosure);
52
+ return Object.freeze({ root, typeClosure });
68
53
  }
69
54
  catch (cause) {
70
55
  rmSync(root, { recursive: true, force: true });
@@ -73,7 +58,109 @@ export function compileDomainPackage(projectDir, input) {
73
58
  finally {
74
59
  rmSync(temporary, { recursive: true, force: true });
75
60
  }
76
- return Object.freeze({ root });
61
+ }
62
+ function admitSchemaClosure(project, configuration) {
63
+ const loaded = ts.readConfigFile(configuration, ts.sys.readFile);
64
+ if (loaded.error !== undefined)
65
+ failDiagnostics([loaded.error], project);
66
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, project, undefined, configuration);
67
+ if (errors(parsed.errors).length > 0)
68
+ failDiagnostics(parsed.errors, project);
69
+ const program = ts.createProgram(parsed.fileNames, parsed.options);
70
+ const local = new Map();
71
+ for (const document of program.getSourceFiles()) {
72
+ const path = relative(project, document.fileName);
73
+ if (path === '..' ||
74
+ path.startsWith(`..${sep}`) ||
75
+ isAbsolute(path) ||
76
+ path === 'node_modules' ||
77
+ path.startsWith(`node_modules${sep}`)) {
78
+ continue;
79
+ }
80
+ local.set(resolve(document.fileName), document);
81
+ }
82
+ for (const document of local.values()) {
83
+ const owner = relative(project, document.fileName).replaceAll('\\', '/');
84
+ if (!owner.startsWith('schema/'))
85
+ continue;
86
+ const visit = (node) => {
87
+ const specifier = referencedModule(node);
88
+ if (specifier !== undefined) {
89
+ const resolved = ts.resolveModuleName(specifier, document.fileName, parsed.options, ts.sys)
90
+ .resolvedModule?.resolvedFileName;
91
+ if (resolved !== undefined) {
92
+ const dependency = local.get(resolve(resolved));
93
+ if (dependency !== undefined) {
94
+ const path = relative(project, dependency.fileName).replaceAll('\\', '/');
95
+ if (!path.startsWith('schema/') && !isTypeOnlyReference(node)) {
96
+ throw new TypeError(`Domain Schema contract imports non-Schema source: ${path}.`);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ts.forEachChild(node, visit);
102
+ };
103
+ visit(document);
104
+ }
105
+ return Object.freeze([...local.values()]
106
+ .map((document) => relative(project, document.fileName).replaceAll('\\', '/'))
107
+ .filter((path) => !path.startsWith('schema/'))
108
+ .sort()
109
+ .map((source) => Object.freeze({
110
+ source,
111
+ target: `schema/.types/${source.replace(/\.(?:tsx?|mts|cts)$/u, '.js')}`,
112
+ })));
113
+ }
114
+ function referencedModule(node) {
115
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
116
+ node.moduleSpecifier !== undefined &&
117
+ ts.isStringLiteral(node.moduleSpecifier)) {
118
+ return node.moduleSpecifier.text;
119
+ }
120
+ if (ts.isImportTypeNode(node) &&
121
+ ts.isLiteralTypeNode(node.argument) &&
122
+ ts.isStringLiteral(node.argument.literal)) {
123
+ return node.argument.literal.text;
124
+ }
125
+ if (ts.isCallExpression(node) &&
126
+ node.arguments.length > 0 &&
127
+ ts.isStringLiteral(node.arguments[0]) &&
128
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
129
+ (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
130
+ return node.arguments[0].text;
131
+ }
132
+ return undefined;
133
+ }
134
+ function isTypeOnlyReference(node) {
135
+ if (ts.isImportTypeNode(node))
136
+ return true;
137
+ if (ts.isExportDeclaration(node))
138
+ return node.isTypeOnly;
139
+ if (!ts.isImportDeclaration(node))
140
+ return false;
141
+ const clause = node.importClause;
142
+ if (clause?.isTypeOnly)
143
+ return true;
144
+ if (clause?.name !== undefined || clause?.namedBindings === undefined)
145
+ return false;
146
+ return (ts.isNamedImports(clause.namedBindings) &&
147
+ clause.namedBindings.elements.length > 0 &&
148
+ clause.namedBindings.elements.every((element) => element.isTypeOnly));
149
+ }
150
+ function relocateTypeClosure(root, closure) {
151
+ for (const { source, target } of closure) {
152
+ const emitted = resolve(root, source.replace(/\.(?:tsx?|mts|cts)$/u, ''));
153
+ const declaration = `${emitted}.d.ts`;
154
+ if (!existsSync(declaration)) {
155
+ throw new TypeError(`Domain contract type closure declaration is absent: ${source}.`);
156
+ }
157
+ const destination = resolve(root, target.replace(/\.js$/u, '.d.ts'));
158
+ mkdirSync(dirname(destination), { recursive: true });
159
+ renameSync(declaration, destination);
160
+ for (const extension of ['.js', '.mjs', '.cjs', '.jsx']) {
161
+ rmSync(`${emitted}${extension}`, { force: true });
162
+ }
163
+ }
77
164
  }
78
165
  function runCompiler(project, arguments_) {
79
166
  const compiler = resolveCompiler();
@@ -113,30 +200,6 @@ function resolveCompiler() {
113
200
  }
114
201
  return compiler;
115
202
  }
116
- function declarationSource(project, output, rootDir, entrypoint) {
117
- const emitted = relative(output, resolve(project, entrypoint));
118
- if (emitted === '..' || emitted.startsWith(`..${sep}`) || isAbsolute(emitted)) {
119
- throw new TypeError(`Published declaration entrypoint escapes package output: ${entrypoint}.`);
120
- }
121
- const source = emitted
122
- .replace(/\.d\.mts$/u, '.mts')
123
- .replace(/\.d\.cts$/u, '.cts')
124
- .replace(/\.d\.ts$/u, '.ts');
125
- if (source === emitted) {
126
- throw new TypeError(`Published declaration entrypoint is not a declaration: ${entrypoint}.`);
127
- }
128
- const file = resolve(rootDir === undefined ? project : rootDir, source);
129
- if (!existsSync(file)) {
130
- throw new TypeError(`Published declaration source is absent: ${source}.`);
131
- }
132
- return file;
133
- }
134
- function confineOutput(project, output) {
135
- const path = relative(project, output);
136
- if (path === '' || path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {
137
- throw new TypeError('Domain package output must be a child of the project root.');
138
- }
139
- }
140
203
  function errors(diagnostics) {
141
204
  return diagnostics.filter(({ category }) => category === ts.DiagnosticCategory.Error);
142
205
  }
@@ -8,6 +8,7 @@ export async function normalizeDeclarations(input) {
8
8
  const root = resolve(input.root);
9
9
  const output = new Map();
10
10
  const admittedFacades = new Map();
11
+ const admittedImports = admitPackageImports(input.imports ?? {});
11
12
  let replacements = 0;
12
13
  const files = reachableDeclarations(root, input.entrypoints, input.imports);
13
14
  const unreachable = allDeclarations(root).filter((file) => !files.includes(file));
@@ -17,6 +18,10 @@ export async function normalizeDeclarations(input) {
17
18
  if (specifier.startsWith('.')) {
18
19
  return runtimeSpecifier(file, resolveLocal(file, specifier));
19
20
  }
21
+ if (specifier.startsWith('#')) {
22
+ const target = resolvePackageImport(specifier, admittedImports);
23
+ return runtimeSpecifier(file, resolveImportTarget(root, target));
24
+ }
20
25
  const key = JSON.stringify([specifier, symbols]);
21
26
  if (admittedFacades.has(key))
22
27
  return admittedFacades.get(key);
@@ -107,7 +112,7 @@ function runtimeSpecifier(containing, declaration) {
107
112
  .replace(/\.d\.cts$/u, '.cjs')
108
113
  .replace(/\.d\.ts$/u, '.js');
109
114
  const path = relative(dirname(containing), runtime).replaceAll('\\', '/');
110
- return path.startsWith('.') ? path : `./${path}`;
115
+ return path.startsWith('./') || path.startsWith('../') ? path : `./${path}`;
111
116
  }
112
117
  function confined(root, file) {
113
118
  const path = relative(realpathSync(root), realpathSync(file));
@@ -4,14 +4,14 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
4
4
  export function verifyPublishedJavaScript(projectDir, manifest) {
5
5
  const project = resolve(projectDir);
6
6
  const targets = new Set();
7
- collectRuntimeTarget(manifest.publishConfig?.main ?? manifest.main, targets, false);
7
+ collectJavaScriptTarget(manifest.publishConfig?.main ?? manifest.main, targets, false);
8
8
  collectExports(manifest.publishConfig?.exports ?? manifest.exports, targets);
9
9
  if (targets.size === 0) {
10
- throw new TypeError('Published Domain package declares no JavaScript runtime export.');
10
+ throw new TypeError('Published Domain contract declares no JavaScript export.');
11
11
  }
12
12
  for (const target of [...targets].sort()) {
13
13
  if (!target.startsWith('./dist/') || !/\.(?:c|m)?js$/u.test(target)) {
14
- throw new TypeError(`Published runtime export must target emitted JavaScript under ./dist: ${target}.`);
14
+ throw new TypeError(`Published contract export must target emitted JavaScript under ./dist: ${target}.`);
15
15
  }
16
16
  const file = resolve(project, target);
17
17
  const path = relative(project, file);
@@ -23,7 +23,7 @@ export function verifyPublishedJavaScript(projectDir, manifest) {
23
23
  }
24
24
  }
25
25
  }
26
- function collectRuntimeTarget(value, targets, types) {
26
+ function collectJavaScriptTarget(value, targets, types) {
27
27
  if (typeof value === 'string') {
28
28
  if (!types)
29
29
  targets.add(value);
@@ -31,24 +31,24 @@ function collectRuntimeTarget(value, targets, types) {
31
31
  }
32
32
  if (Array.isArray(value)) {
33
33
  for (const nested of value)
34
- collectRuntimeTarget(nested, targets, types);
34
+ collectJavaScriptTarget(nested, targets, types);
35
35
  return;
36
36
  }
37
37
  if (value === null || typeof value !== 'object')
38
38
  return;
39
39
  for (const [condition, nested] of Object.entries(value)) {
40
- collectRuntimeTarget(nested, targets, types || condition === 'types');
40
+ collectJavaScriptTarget(nested, targets, types || condition === 'types');
41
41
  }
42
42
  }
43
43
  function collectExports(value, targets) {
44
44
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
45
- collectRuntimeTarget(value, targets, false);
45
+ collectJavaScriptTarget(value, targets, false);
46
46
  return;
47
47
  }
48
48
  const entries = Object.entries(value);
49
49
  const subpaths = entries.filter(([key]) => key.startsWith('.'));
50
50
  if (subpaths.length === 0) {
51
- collectRuntimeTarget(value, targets, false);
51
+ collectJavaScriptTarget(value, targets, false);
52
52
  return;
53
53
  }
54
54
  if (subpaths.length !== entries.length) {
@@ -56,6 +56,6 @@ function collectExports(value, targets) {
56
56
  }
57
57
  for (const [subpath, target] of subpaths) {
58
58
  if (subpath !== './package.json')
59
- collectRuntimeTarget(target, targets, false);
59
+ collectJavaScriptTarget(target, targets, false);
60
60
  }
61
61
  }
@@ -0,0 +1,2 @@
1
+ /** Verify emitted Schema JavaScript and return its complete external package set. */
2
+ export declare function verifyContractJavaScript(root: string): readonly string[];
@@ -1,32 +1,30 @@
1
- import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
3
  import ts from 'typescript';
4
- /** Reject direct Kernel runtime imports from a packaged product Domain. */
5
- export function verifyRuntimeFacades(root) {
6
- for (const file of runtimeFiles(root)) {
4
+ /** Verify emitted Schema JavaScript and return its complete external package set. */
5
+ export function verifyContractJavaScript(root) {
6
+ const packages = new Set();
7
+ for (const file of javascriptFiles(root)) {
7
8
  const source = readFileSync(file, 'utf8');
8
9
  const document = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
9
10
  visit(document);
10
11
  function visit(node) {
11
12
  const specifier = moduleSpecifier(node);
12
13
  if (specifier?.startsWith('@astrale-os/kernel-')) {
13
- throw new TypeError(`Domain runtime imports a Kernel package directly: ${specifier} in ${file}.`);
14
+ throw new TypeError(`Domain contract imports a Kernel package directly: ${specifier} in ${file}.`);
15
+ }
16
+ if (specifier !== undefined &&
17
+ !specifier.startsWith('.') &&
18
+ !specifier.startsWith('#') &&
19
+ !specifier.startsWith('node:')) {
20
+ packages.add(packageName(specifier));
14
21
  }
15
22
  ts.forEachChild(node, visit);
16
23
  }
17
24
  }
25
+ return Object.freeze([...packages].sort());
18
26
  }
19
- /** Publish only the admitted Runtime identity; handlers and providers remain package-private. */
20
- export function writeRuntimeDeclaration(root, entrypoint) {
21
- const file = resolve(root, entrypoint);
22
- const path = relative(resolve(root), file);
23
- if (path === '' || path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {
24
- throw new TypeError('Runtime declaration entrypoint escapes package output.');
25
- }
26
- mkdirSync(dirname(file), { recursive: true });
27
- writeFileSync(file, "import type { Runtime } from '@astrale-os/sdk/runtime'\n\ndeclare const runtime: Runtime\nexport default runtime\n");
28
- }
29
- function runtimeFiles(root) {
27
+ function javascriptFiles(root) {
30
28
  const files = [];
31
29
  const visit = (directory) => {
32
30
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
@@ -46,7 +44,7 @@ function moduleSpecifier(node) {
46
44
  ts.isStringLiteral(node.moduleSpecifier)) {
47
45
  return node.moduleSpecifier.text;
48
46
  }
49
- if (!ts.isCallExpression(node) || node.arguments.length !== 1)
47
+ if (!ts.isCallExpression(node) || node.arguments.length === 0)
50
48
  return undefined;
51
49
  const dynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
52
50
  const requireCall = ts.isIdentifier(node.expression) && node.expression.text === 'require';
@@ -55,3 +53,7 @@ function moduleSpecifier(node) {
55
53
  ? argument.text
56
54
  : undefined;
57
55
  }
56
+ function packageName(specifier) {
57
+ const parts = specifier.split('/');
58
+ return specifier.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
59
+ }
@@ -6,5 +6,5 @@ export interface PackageDomainResult {
6
6
  readonly replacements: number;
7
7
  readonly packages: readonly string[];
8
8
  }
9
- /** Compile, normalize, and verify one publishable Domain runtime package. */
9
+ /** Compile, normalize, and verify one public Domain Schema contract package. */
10
10
  export declare function packageDomain(input: PackageDomainInput): Promise<PackageDomainResult>;
@@ -1,92 +1,120 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { rm } from 'node:fs/promises';
2
3
  import { resolve } from 'node:path';
3
4
  import { admitDomainPackage } from './admission.js';
4
5
  import { compileDomainPackage } from './compile.js';
5
6
  import { normalizeDeclarations, verifyDeclarationPackage } from './declarations/index.js';
6
7
  import { verifyPublishedJavaScript } from './exports.js';
7
- import { verifyRuntimeFacades, writeRuntimeDeclaration } from './runtime.js';
8
- /** Compile, normalize, and verify one publishable Domain runtime package. */
8
+ import { verifyContractJavaScript } from './javascript.js';
9
+ /** Compile, normalize, and verify one public Domain Schema contract package. */
9
10
  export async function packageDomain(input) {
10
11
  const projectDir = resolve(input.projectDir);
11
12
  const manifest = JSON.parse(await readFile(resolve(projectDir, 'package.json'), 'utf8'));
12
13
  admitDomainPackage(projectDir, manifest);
13
- const publishedEntrypoints = declarationEntrypoints(manifest);
14
- const publishedRuntime = runtimeDeclarationEntrypoint(manifest);
15
- const { root } = compileDomainPackage(projectDir, {
16
- declarations: publishedEntrypoints.filter((entrypoint) => entrypoint !== publishedRuntime),
17
- });
18
- const output = relativeOutputRoot(projectDir, root);
19
- const entrypoints = publishedEntrypoints.map((entrypoint) => relativeOutput(entrypoint, output));
20
- const runtimeEntrypoint = publishedRuntime === undefined ? undefined : relativeOutput(publishedRuntime, output);
21
- if (runtimeEntrypoint !== undefined)
22
- writeRuntimeDeclaration(root, runtimeEntrypoint);
23
- verifyPublishedJavaScript(projectDir, manifest);
24
- const normalized = await normalizeDeclarations({
25
- root,
26
- imports: manifest.publishConfig?.imports ?? manifest.imports,
27
- entrypoints,
28
- });
29
- verifyRuntimeFacades(root);
30
- const verified = await verifyDeclarationPackage({ root, packageRoot: projectDir, entrypoints });
31
- return Object.freeze({
32
- declarations: verified.files.length,
33
- replacements: normalized.replacements,
34
- packages: verified.packages,
35
- });
36
- }
37
- function relativeOutputRoot(project, output) {
38
- return output.slice(project.length + 1).replaceAll('\\', '/');
14
+ admitContractManifest(manifest);
15
+ const { root, typeClosure } = compileDomainPackage(projectDir);
16
+ try {
17
+ const entrypoints = ['schema/index.d.ts'];
18
+ verifyPublishedJavaScript(projectDir, manifest);
19
+ const javascriptPackages = verifyContractJavaScript(root);
20
+ const normalized = await normalizeDeclarations({
21
+ root,
22
+ imports: declarationImports(manifest, typeClosure),
23
+ entrypoints,
24
+ });
25
+ const verified = await verifyDeclarationPackage({ root, packageRoot: projectDir, entrypoints });
26
+ const packages = Object.freeze([...new Set([...javascriptPackages, ...verified.packages])].sort());
27
+ verifyDependencies(manifest, packages);
28
+ return Object.freeze({
29
+ declarations: verified.files.length,
30
+ replacements: normalized.replacements,
31
+ packages,
32
+ });
33
+ }
34
+ catch (cause) {
35
+ await rm(root, { recursive: true, force: true });
36
+ throw cause;
37
+ }
39
38
  }
40
- function declarationEntrypoints(manifest) {
41
- const values = new Set();
42
- const collect = (value, types = false) => {
43
- if (typeof value === 'string') {
44
- if (types || /\.d\.(?:ts|mts|cts)$/u.test(value))
45
- values.add(normalizePath(value));
46
- return;
39
+ function declarationImports(manifest, closure) {
40
+ const imports = { ...manifest.publishConfig?.imports };
41
+ for (const { source, target } of closure) {
42
+ const specifier = packageImportForSource(manifest.imports ?? {}, source);
43
+ if (specifier === undefined) {
44
+ throw new TypeError(`Domain contract type closure has no package import: ${source}.`);
47
45
  }
48
- if (!value || typeof value !== 'object')
49
- return;
50
- for (const [condition, nested] of Object.entries(value))
51
- collect(nested, condition === 'types');
52
- };
53
- const published = manifest.publishConfig;
54
- collect(published?.types ?? manifest.types, true);
55
- collect(published?.exports ?? manifest.exports);
56
- if (values.size === 0)
57
- throw new TypeError('Published Domain package declares no TypeScript entrypoint.');
58
- return Object.freeze([...values].sort());
46
+ imports[specifier] = `./dist/${target}`;
47
+ }
48
+ return Object.keys(imports).length === 0 ? undefined : imports;
59
49
  }
60
- function runtimeDeclarationEntrypoint(manifest) {
61
- const exports = manifest.publishConfig?.exports ?? manifest.exports;
62
- if (exports === null || typeof exports !== 'object' || Array.isArray(exports))
63
- return undefined;
64
- const runtime = exports['./runtime'];
65
- if (runtime === undefined)
66
- return undefined;
67
- const values = [];
68
- const collect = (value, types = false) => {
69
- if (typeof value === 'string') {
70
- if (types || /\.d\.(?:ts|mts|cts)$/u.test(value))
71
- values.push(normalizePath(value));
72
- return;
50
+ function packageImportForSource(imports, source) {
51
+ for (const [specifier, target] of Object.entries(imports).sort(([left], [right]) => left.localeCompare(right))) {
52
+ if (typeof target !== 'string')
53
+ continue;
54
+ const normalized = target.replace(/^\.\//u, '');
55
+ const wildcard = normalized.indexOf('*');
56
+ if (wildcard === -1) {
57
+ if (normalized === source)
58
+ return specifier;
59
+ continue;
73
60
  }
74
- if (value === null || typeof value !== 'object')
75
- return;
76
- for (const [condition, nested] of Object.entries(value))
77
- collect(nested, condition === 'types');
78
- };
79
- collect(runtime);
80
- if (values.length !== 1) {
81
- throw new TypeError('Published Domain ./runtime must declare exactly one TypeScript entrypoint.');
61
+ const prefix = normalized.slice(0, wildcard);
62
+ const suffix = normalized.slice(wildcard + 1);
63
+ if (!source.startsWith(prefix) || !source.endsWith(suffix))
64
+ continue;
65
+ const value = source.slice(prefix.length, source.length - suffix.length);
66
+ return specifier.replace('*', value);
67
+ }
68
+ return undefined;
69
+ }
70
+ function admitContractManifest(manifest) {
71
+ if (manifest.files?.length !== 1 || manifest.files[0] !== 'dist') {
72
+ throw new TypeError('Domain contract package files must be exactly ["dist"].');
73
+ }
74
+ if (manifest.main !== './schema/index.ts' || manifest.types !== './schema/index.ts') {
75
+ throw new TypeError('Domain contract source entrypoint must be ./schema/index.ts.');
82
76
  }
83
- return values[0];
77
+ admitExports(manifest.exports, './schema/index.ts', './schema/index.ts', 'source');
78
+ const published = manifest.publishConfig;
79
+ if (published?.main !== './dist/schema/index.js' ||
80
+ published.types !== './dist/schema/index.d.ts') {
81
+ throw new TypeError('Published Domain contract entrypoint must be ./dist/schema/index.js.');
82
+ }
83
+ admitExports(published.exports, './dist/schema/index.d.ts', './dist/schema/index.js', 'published');
84
84
  }
85
- function normalizePath(file) {
86
- return file.replace(/^\.\//u, '').replaceAll('\\', '/');
85
+ function admitExports(exports, types, imported, label) {
86
+ if (exports === null || typeof exports !== 'object' || Array.isArray(exports)) {
87
+ throw new TypeError(`Domain contract ${label} exports must be an object.`);
88
+ }
89
+ const record = exports;
90
+ const keys = Object.keys(record).sort();
91
+ if (keys.length !== 2 || keys[0] !== '.' || keys[1] !== './package.json') {
92
+ throw new TypeError(`Domain contract ${label} exports must contain only "." and "./package.json".`);
93
+ }
94
+ const root = record['.'];
95
+ if (root === undefined ||
96
+ typeof root !== 'object' ||
97
+ root.types !== types ||
98
+ root.import !== imported) {
99
+ throw new TypeError(`Domain contract ${label} root export is invalid.`);
100
+ }
101
+ if (record['./package.json'] !== './package.json') {
102
+ throw new TypeError(`Domain contract ${label} package metadata export is invalid.`);
103
+ }
87
104
  }
88
- function relativeOutput(file, output) {
89
- const normalized = file.replace(/^\.\//u, '');
90
- const prefix = `${output.replace(/^\.\//u, '').replace(/\/$/u, '')}/`;
91
- return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
105
+ function verifyDependencies(manifest, packages) {
106
+ const declared = new Set([
107
+ ...Object.keys(manifest.dependencies ?? {}),
108
+ ...Object.keys(manifest.optionalDependencies ?? {}),
109
+ ...Object.keys(manifest.peerDependencies ?? {}),
110
+ ]);
111
+ const used = new Set(packages);
112
+ const missing = packages.filter((name) => !declared.has(name));
113
+ if (missing.length > 0) {
114
+ throw new TypeError(`Published Domain contract references undeclared packages: ${missing.join(', ')}.`);
115
+ }
116
+ const unused = [...declared].filter((name) => !used.has(name)).sort();
117
+ if (unused.length > 0) {
118
+ throw new TypeError(`Domain contract declares unused production packages: ${unused.join(', ')}.`);
119
+ }
92
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.43",
3
+ "version": "0.5.0-beta.44",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",
@@ -1,4 +0,0 @@
1
- /** Reject direct Kernel runtime imports from a packaged product Domain. */
2
- export declare function verifyRuntimeFacades(root: string): void;
3
- /** Publish only the admitted Runtime identity; handlers and providers remain package-private. */
4
- export declare function writeRuntimeDeclaration(root: string, entrypoint: string): void;