@hypequery/cli 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -0
- package/dist/commands/deployment.d.ts +12 -0
- package/dist/commands/deployment.d.ts.map +1 -0
- package/dist/commands/deployment.js +145 -0
- package/dist/utils/deployment-runtime-artifact.d.ts +9 -0
- package/dist/utils/deployment-runtime-artifact.d.ts.map +1 -0
- package/dist/utils/deployment-runtime-artifact.js +142 -0
- package/dist/utils/load-api.d.ts +1 -0
- package/dist/utils/load-api.d.ts.map +1 -1
- package/dist/utils/load-api.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Use it to:
|
|
|
7
7
|
- generate schema types from ClickHouse or embedded chDB
|
|
8
8
|
- scaffold `analytics/` files
|
|
9
9
|
- run the local dev server with docs
|
|
10
|
+
- build and validate portable deployment contracts
|
|
10
11
|
|
|
11
12
|
## Quick Start
|
|
12
13
|
|
|
@@ -135,6 +136,40 @@ npx hypequery generate:manifest analytics/api.ts --output analytics/hypequery-ma
|
|
|
135
136
|
The output is the exact serializable JSON returned by `api.manifest()`, including
|
|
136
137
|
semantic keys such as `dataset:orders`.
|
|
137
138
|
|
|
139
|
+
### `hypequery deployment:build`
|
|
140
|
+
|
|
141
|
+
Builds deployment metadata for an exported HypeQuery API and writes the
|
|
142
|
+
artifact with its identity sidecar.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx hypequery deployment:build analytics/api.ts
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Named Serve handlers are bundled into a Node runtime artifact automatically.
|
|
149
|
+
Dataset-only APIs do not produce a runtime artifact. Use `--runtime-artifact`
|
|
150
|
+
to reference a separately built Node or Python artifact instead.
|
|
151
|
+
|
|
152
|
+
Options:
|
|
153
|
+
|
|
154
|
+
- `--output <path>`: default `analytics/hypequery-deployment.json`
|
|
155
|
+
- `--runtime <runtime>`: `node` (default) or `python`
|
|
156
|
+
- `--runtime-artifact <sha256>`: lowercase SHA-256 of a prebuilt runtime artifact
|
|
157
|
+
- `--runtime-output <path>`: default beside the deployment JSON as `hypequery-runtime.mjs`
|
|
158
|
+
- `--entrypoint-prefix <prefix>`: default `queries`
|
|
159
|
+
- `--hash-output <path>`: default `<output>.sha256`
|
|
160
|
+
|
|
161
|
+
The identity sidecar is not compatible with `sha256sum -c`. Use
|
|
162
|
+
`deployment:validate` to validate the artifact and report its identity.
|
|
163
|
+
|
|
164
|
+
### `hypequery deployment:validate`
|
|
165
|
+
|
|
166
|
+
Validates an existing deployment artifact and reports its datasets, queries,
|
|
167
|
+
runtime artifacts, and identity.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx hypequery deployment:validate analytics/hypequery-deployment.json
|
|
171
|
+
```
|
|
172
|
+
|
|
138
173
|
## Non-interactive Setup
|
|
139
174
|
|
|
140
175
|
For ClickHouse, `hypequery init --no-interactive` reads:
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAyJD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { devCommand } from './commands/dev.js';
|
|
|
6
6
|
import { generateCommand } from './commands/generate.js';
|
|
7
7
|
import { generateDatasetsCommand } from './commands/generate-datasets.js';
|
|
8
8
|
import { generateManifestCommand } from './commands/generate-manifest.js';
|
|
9
|
+
import { buildDeploymentCommand, validateDeploymentCommand, } from './commands/deployment.js';
|
|
9
10
|
const program = new Command();
|
|
10
11
|
function getCliVersion() {
|
|
11
12
|
try {
|
|
@@ -108,6 +109,24 @@ program
|
|
|
108
109
|
.action(runCommand(async (api, options) => {
|
|
109
110
|
await generateManifestCommand(api, options);
|
|
110
111
|
}));
|
|
112
|
+
program
|
|
113
|
+
.command('deployment:build <api>')
|
|
114
|
+
.description('Build a validated, canonical deployment contract and identity')
|
|
115
|
+
.option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
|
|
116
|
+
.option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
|
|
117
|
+
.option('--runtime-artifact <sha256>', 'Use a prebuilt runtime artifact with this SHA-256 identity')
|
|
118
|
+
.option('--runtime-output <path>', 'Bundled Node runtime path (default: beside deployment JSON)')
|
|
119
|
+
.option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
|
|
120
|
+
.option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
|
|
121
|
+
.action(runCommand(async (api, options) => {
|
|
122
|
+
await buildDeploymentCommand(api, options);
|
|
123
|
+
}));
|
|
124
|
+
program
|
|
125
|
+
.command('deployment:validate <artifact>')
|
|
126
|
+
.description('Validate a deployment contract and report its identity')
|
|
127
|
+
.action(runCommand(async (artifact) => {
|
|
128
|
+
await validateDeploymentCommand(artifact);
|
|
129
|
+
}));
|
|
111
130
|
// Help command
|
|
112
131
|
program
|
|
113
132
|
.command('help [command]')
|
|
@@ -138,6 +157,8 @@ program.on('--help', () => {
|
|
|
138
157
|
console.log(' hypequery generate:types --output analytics/schema.ts');
|
|
139
158
|
console.log(' hypequery generate:datasets');
|
|
140
159
|
console.log(' hypequery generate:manifest analytics/api.ts --output analytics/hypequery-manifest.json');
|
|
160
|
+
console.log(' hypequery deployment:build analytics/api.ts --runtime-artifact <sha256>');
|
|
161
|
+
console.log(' hypequery deployment:validate analytics/hypequery-deployment.json');
|
|
141
162
|
console.log('');
|
|
142
163
|
console.log('Docs: https://hypequery.com/docs');
|
|
143
164
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ProtocolDeploymentContract } from '@hypequery/protocol';
|
|
2
|
+
export interface BuildDeploymentOptions {
|
|
3
|
+
output?: string;
|
|
4
|
+
runtime?: 'node' | 'python';
|
|
5
|
+
runtimeArtifact?: string;
|
|
6
|
+
runtimeOutput?: string;
|
|
7
|
+
entrypointPrefix?: string;
|
|
8
|
+
hashOutput?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildDeploymentCommand(apiPath: string | undefined, options?: BuildDeploymentOptions): Promise<ProtocolDeploymentContract>;
|
|
11
|
+
export declare function validateDeploymentCommand(artifactPath: string | undefined): Promise<ProtocolDeploymentContract>;
|
|
12
|
+
//# sourceMappingURL=deployment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../src/commands/deployment.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAW7B,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAkDD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CA0FrC;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAoCrC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { prepareProtocolDeploymentContract, } from '@hypequery/protocol';
|
|
4
|
+
import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
|
|
5
|
+
import { loadApiModule } from '../utils/load-api.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
8
|
+
function runtimeName(options) {
|
|
9
|
+
const runtime = options.runtime ?? 'node';
|
|
10
|
+
if (runtime !== 'node' && runtime !== 'python') {
|
|
11
|
+
throw new Error('--runtime must be either node or python.');
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
|
15
|
+
function configuredRuntimeArtifact(options, runtime) {
|
|
16
|
+
if (options.runtimeArtifact === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
|
|
19
|
+
throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
runtime,
|
|
23
|
+
artifactSha256: options.runtimeArtifact,
|
|
24
|
+
...(options.entrypointPrefix !== undefined
|
|
25
|
+
? { entrypointPrefix: options.entrypointPrefix }
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function assertDistinctOutputPaths(paths) {
|
|
30
|
+
const seen = new Map();
|
|
31
|
+
for (const [label, value] of Object.entries(paths)) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
const resolved = path.resolve(value);
|
|
35
|
+
const existing = seen.get(resolved);
|
|
36
|
+
if (existing) {
|
|
37
|
+
throw new Error(`${label} must use a different path from ${existing}.`);
|
|
38
|
+
}
|
|
39
|
+
seen.set(resolved, label);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function buildDeploymentCommand(apiPath, options = {}) {
|
|
43
|
+
if (!apiPath) {
|
|
44
|
+
throw new Error('Missing API module path.\n\n'
|
|
45
|
+
+ 'Usage: hypequery deployment:build analytics/api.ts');
|
|
46
|
+
}
|
|
47
|
+
const runtime = runtimeName(options);
|
|
48
|
+
const configuredArtifact = configuredRuntimeArtifact(options, runtime);
|
|
49
|
+
if (configuredArtifact && options.runtimeOutput !== undefined) {
|
|
50
|
+
throw new Error('--runtime-output cannot be used with a prebuilt --runtime-artifact.');
|
|
51
|
+
}
|
|
52
|
+
const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
|
|
53
|
+
const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
|
|
54
|
+
if (options.runtimeOutput !== undefined) {
|
|
55
|
+
assertDistinctOutputPaths({
|
|
56
|
+
'--output': outputPath,
|
|
57
|
+
'--hash-output': hashOutputPath,
|
|
58
|
+
'--runtime-output': options.runtimeOutput,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const api = await loadApiModule(apiPath);
|
|
62
|
+
if (typeof api.deploymentContract !== 'function') {
|
|
63
|
+
throw new Error(`Invalid API module: ${apiPath}\n\n`
|
|
64
|
+
+ 'The exported API must provide deploymentContract(). '
|
|
65
|
+
+ 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
|
|
66
|
+
}
|
|
67
|
+
let builtArtifact;
|
|
68
|
+
let artifact = configuredArtifact;
|
|
69
|
+
if (!artifact) {
|
|
70
|
+
const runtimeEntrypoints = getDeploymentRuntimeEntrypoints(api);
|
|
71
|
+
if (runtimeEntrypoints.length > 0) {
|
|
72
|
+
if (runtime !== 'node') {
|
|
73
|
+
throw new Error('Automatic runtime artifact builds currently support Node only. '
|
|
74
|
+
+ 'Provide --runtime-artifact for Python deployments.');
|
|
75
|
+
}
|
|
76
|
+
builtArtifact = await buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, options.entrypointPrefix);
|
|
77
|
+
artifact = {
|
|
78
|
+
runtime: 'node',
|
|
79
|
+
artifactSha256: builtArtifact.artifactSha256,
|
|
80
|
+
entrypointPrefix: builtArtifact.entrypointPrefix,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const contract = api.deploymentContract(artifact ? { runtimeArtifact: artifact } : {});
|
|
85
|
+
const prepared = prepareProtocolDeploymentContract(contract);
|
|
86
|
+
const { canonical, contract: validated, identity: digest } = prepared;
|
|
87
|
+
const runtimeOutputPath = builtArtifact
|
|
88
|
+
? options.runtimeOutput ?? path.join(path.dirname(outputPath), 'hypequery-runtime.mjs')
|
|
89
|
+
: undefined;
|
|
90
|
+
assertDistinctOutputPaths({
|
|
91
|
+
'--output': outputPath,
|
|
92
|
+
'--hash-output': hashOutputPath,
|
|
93
|
+
'--runtime-output': runtimeOutputPath,
|
|
94
|
+
});
|
|
95
|
+
const identitySidecar = [
|
|
96
|
+
'# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
|
|
97
|
+
'# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
|
|
98
|
+
+ 'the output newline is excluded.',
|
|
99
|
+
`${digest} ${path.basename(outputPath)}`,
|
|
100
|
+
'',
|
|
101
|
+
].join('\n');
|
|
102
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
103
|
+
await mkdir(path.dirname(hashOutputPath), { recursive: true });
|
|
104
|
+
if (runtimeOutputPath && builtArtifact) {
|
|
105
|
+
await mkdir(path.dirname(runtimeOutputPath), { recursive: true });
|
|
106
|
+
await writeFile(runtimeOutputPath, builtArtifact.bytes);
|
|
107
|
+
}
|
|
108
|
+
await writeFile(outputPath, `${canonical}\n`, 'utf8');
|
|
109
|
+
await writeFile(hashOutputPath, identitySidecar, 'utf8');
|
|
110
|
+
if (runtimeOutputPath && builtArtifact) {
|
|
111
|
+
logger.success(`Runtime artifact written to ${runtimeOutputPath}`);
|
|
112
|
+
logger.info(`Runtime artifact SHA-256: ${builtArtifact.artifactSha256}`);
|
|
113
|
+
}
|
|
114
|
+
logger.success(`Deployment contract written to ${outputPath}`);
|
|
115
|
+
logger.info(`Identity: ${digest}`);
|
|
116
|
+
return validated;
|
|
117
|
+
}
|
|
118
|
+
export async function validateDeploymentCommand(artifactPath) {
|
|
119
|
+
if (!artifactPath) {
|
|
120
|
+
throw new Error('Missing deployment artifact path.\n\n'
|
|
121
|
+
+ 'Usage: hypequery deployment:validate analytics/hypequery-deployment.json');
|
|
122
|
+
}
|
|
123
|
+
let input;
|
|
124
|
+
try {
|
|
125
|
+
input = JSON.parse(await readFile(artifactPath, 'utf8'));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new Error(`Invalid deployment JSON: ${artifactPath}\n\n`
|
|
129
|
+
+ (error instanceof Error ? error.message : String(error)));
|
|
130
|
+
}
|
|
131
|
+
let prepared;
|
|
132
|
+
try {
|
|
133
|
+
prepared = prepareProtocolDeploymentContract(input);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`Invalid deployment contract: ${artifactPath}\n\n`
|
|
137
|
+
+ (error instanceof Error ? error.message : String(error)));
|
|
138
|
+
}
|
|
139
|
+
const { contract, identity: digest } = prepared;
|
|
140
|
+
logger.success(`Valid deployment contract: ${artifactPath}`);
|
|
141
|
+
logger.info(`${contract.datasets.length} datasets, ${contract.queries.length} queries, `
|
|
142
|
+
+ `${contract.artifacts.length} runtime artifacts`);
|
|
143
|
+
logger.info(`Identity: ${digest}`);
|
|
144
|
+
return contract;
|
|
145
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface NodeRuntimeArtifact {
|
|
2
|
+
readonly bytes: Uint8Array;
|
|
3
|
+
readonly artifactSha256: string;
|
|
4
|
+
readonly entrypointPrefix: string;
|
|
5
|
+
readonly runtimeEntrypoints: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function getDeploymentRuntimeEntrypoints(input: unknown): readonly string[];
|
|
8
|
+
export declare function buildNodeRuntimeArtifact(apiPath: string, runtimeEntrypoints: readonly string[], entrypointPrefix?: string): Promise<NodeRuntimeArtifact>;
|
|
9
|
+
//# sourceMappingURL=deployment-runtime-artifact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-runtime-artifact.d.ts","sourceRoot":"","sources":["../../src/utils/deployment-runtime-artifact.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD;AASD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CA6BjF;AA6CD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,kBAAkB,EAAE,SAAS,MAAM,EAAE,EACrC,gBAAgB,SAAY,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, } from '@hypequery/protocol';
|
|
4
|
+
import { build } from 'esbuild';
|
|
5
|
+
import { findNearestTsconfig } from './load-api.js';
|
|
6
|
+
const deploymentBuildSourceSymbol = Symbol.for('hypequery.deployment-build-source.v1');
|
|
7
|
+
function executableQuery(query) {
|
|
8
|
+
return typeof query === 'function'
|
|
9
|
+
|| (typeof query === 'object'
|
|
10
|
+
&& query !== null
|
|
11
|
+
&& typeof query.run === 'function');
|
|
12
|
+
}
|
|
13
|
+
export function getDeploymentRuntimeEntrypoints(input) {
|
|
14
|
+
if (typeof input !== 'object' || input === null) {
|
|
15
|
+
throw new Error('Invalid API module: deployment build metadata is unavailable.');
|
|
16
|
+
}
|
|
17
|
+
const api = input;
|
|
18
|
+
const source = api[deploymentBuildSourceSymbol];
|
|
19
|
+
if (source?.version !== 1 || !Array.isArray(source.runtimeEntrypoints)) {
|
|
20
|
+
throw new Error('The exported API does not expose deployment build metadata. '
|
|
21
|
+
+ 'Upgrade @hypequery/serve before building runtime artifacts.');
|
|
22
|
+
}
|
|
23
|
+
const names = source.runtimeEntrypoints.map((name) => {
|
|
24
|
+
try {
|
|
25
|
+
return parseProtocolIdentifier(name);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`Invalid runtime entrypoint name: ${String(name)}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (new Set(names).size !== names.length) {
|
|
32
|
+
throw new Error('Deployment build metadata contains duplicate runtime entrypoints.');
|
|
33
|
+
}
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
if (!executableQuery(api.queries?.[name]?.query)) {
|
|
36
|
+
throw new Error(`Serve query "${name}" does not expose an executable runtime handler.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze([...names].sort());
|
|
40
|
+
}
|
|
41
|
+
function projectRelativeImport(apiPath) {
|
|
42
|
+
const resolved = path.resolve(process.cwd(), apiPath);
|
|
43
|
+
const relative = path.relative(process.cwd(), resolved);
|
|
44
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
45
|
+
throw new Error('The API module must be inside the current project for deterministic bundling.');
|
|
46
|
+
}
|
|
47
|
+
const portable = relative.split(path.sep).join('/');
|
|
48
|
+
return {
|
|
49
|
+
resolved,
|
|
50
|
+
specifier: portable.startsWith('.') ? portable : `./${portable}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function runtimeEntrySource(apiSpecifier, runtimeEntrypoints, entrypointPrefix) {
|
|
54
|
+
const prefix = parseProtocolQualifiedIdentifier(entrypointPrefix).split('.');
|
|
55
|
+
const names = JSON.stringify(runtimeEntrypoints);
|
|
56
|
+
let registryExpression = 'runtimeEntries';
|
|
57
|
+
for (let index = prefix.length - 1; index >= 1; index -= 1) {
|
|
58
|
+
registryExpression = `Object.freeze({ ${prefix[index]}: ${registryExpression} })`;
|
|
59
|
+
}
|
|
60
|
+
return [
|
|
61
|
+
`import * as apiModule from ${JSON.stringify(apiSpecifier)};`,
|
|
62
|
+
'const api = apiModule.api ?? Reflect.get(apiModule, "default");',
|
|
63
|
+
'if (!api || typeof api !== "object") throw new Error("Runtime API export is unavailable.");',
|
|
64
|
+
'const resolveQuery = (name) => {',
|
|
65
|
+
' const query = api.queries?.[name]?.query;',
|
|
66
|
+
' if (typeof query === "function") return query;',
|
|
67
|
+
' if (query && typeof query === "object" && typeof query.run === "function") {',
|
|
68
|
+
' return query.run.bind(query);',
|
|
69
|
+
' }',
|
|
70
|
+
' throw new Error(`Runtime entrypoint ${name} is unavailable.`);',
|
|
71
|
+
'};',
|
|
72
|
+
`const runtimeEntries = Object.freeze(Object.fromEntries(${names}.map(name => [name, resolveQuery(name)])));`,
|
|
73
|
+
`export const ${prefix[0]} = ${registryExpression};`,
|
|
74
|
+
'',
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
77
|
+
export async function buildNodeRuntimeArtifact(apiPath, runtimeEntrypoints, entrypointPrefix = 'queries') {
|
|
78
|
+
if (runtimeEntrypoints.length === 0) {
|
|
79
|
+
throw new Error('Cannot build a runtime artifact without Serve query entrypoints.');
|
|
80
|
+
}
|
|
81
|
+
const names = getValidatedEntrypointNames(runtimeEntrypoints);
|
|
82
|
+
const { resolved, specifier } = projectRelativeImport(apiPath);
|
|
83
|
+
let source;
|
|
84
|
+
try {
|
|
85
|
+
source = runtimeEntrySource(specifier, names, entrypointPrefix);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`Invalid runtime entrypoint prefix: ${entrypointPrefix}`);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const result = await build({
|
|
92
|
+
stdin: {
|
|
93
|
+
contents: source,
|
|
94
|
+
loader: 'ts',
|
|
95
|
+
resolveDir: process.cwd(),
|
|
96
|
+
sourcefile: 'hypequery-runtime-entry.ts',
|
|
97
|
+
},
|
|
98
|
+
absWorkingDir: process.cwd(),
|
|
99
|
+
bundle: true,
|
|
100
|
+
charset: 'utf8',
|
|
101
|
+
format: 'esm',
|
|
102
|
+
legalComments: 'none',
|
|
103
|
+
logLevel: 'warning',
|
|
104
|
+
minify: true,
|
|
105
|
+
platform: 'node',
|
|
106
|
+
sourcemap: false,
|
|
107
|
+
target: ['node18'],
|
|
108
|
+
treeShaking: true,
|
|
109
|
+
tsconfig: await findNearestTsconfig(resolved) ?? undefined,
|
|
110
|
+
write: false,
|
|
111
|
+
});
|
|
112
|
+
const output = result.outputFiles?.find(file => file.path.endsWith('.js'))
|
|
113
|
+
?? result.outputFiles?.[0];
|
|
114
|
+
if (!output)
|
|
115
|
+
throw new Error('esbuild produced no output');
|
|
116
|
+
const bytes = output.contents;
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
bytes,
|
|
119
|
+
artifactSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
120
|
+
entrypointPrefix,
|
|
121
|
+
runtimeEntrypoints: names,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw new Error(`Failed to build the Node runtime artifact from ${apiPath}.\n`
|
|
126
|
+
+ `Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function getValidatedEntrypointNames(input) {
|
|
130
|
+
const names = input.map((name) => {
|
|
131
|
+
try {
|
|
132
|
+
return parseProtocolIdentifier(name);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error(`Invalid runtime entrypoint name: ${name}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (new Set(names).size !== names.length) {
|
|
139
|
+
throw new Error('Runtime entrypoint names must be unique.');
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze([...names].sort());
|
|
142
|
+
}
|
package/dist/utils/load-api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function loadApiModule(modulePath: string): Promise<any>;
|
|
2
2
|
export declare function loadModule(modulePath: string): Promise<any>;
|
|
3
|
+
export declare function findNearestTsconfig(filePath: string): Promise<string | null>;
|
|
3
4
|
//# sourceMappingURL=load-api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-api.d.ts","sourceRoot":"","sources":["../../src/utils/load-api.ts"],"names":[],"mappings":"AAaA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,gBA2DrD;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,gBAkClD"}
|
|
1
|
+
{"version":3,"file":"load-api.d.ts","sourceRoot":"","sources":["../../src/utils/load-api.ts"],"names":[],"mappings":"AAaA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,gBA2DrD;AAED,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,gBAkClD;AA0JD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,0BA8BzD"}
|
package/dist/utils/load-api.js
CHANGED
|
@@ -217,7 +217,7 @@ async function bundleTypeScriptModule(entryPath) {
|
|
|
217
217
|
`Original error: ${error?.message ?? error}`);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
async function findNearestTsconfig(filePath) {
|
|
220
|
+
export async function findNearestTsconfig(filePath) {
|
|
221
221
|
let dir = path.dirname(filePath);
|
|
222
222
|
const visited = [];
|
|
223
223
|
while (dir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Command-line interface for hypequery",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"esbuild": "^0.25.0",
|
|
19
19
|
"open": "^10.0.0",
|
|
20
20
|
"ora": "^8.0.1",
|
|
21
|
-
"prompts": "^2.4.2"
|
|
21
|
+
"prompts": "^2.4.2",
|
|
22
|
+
"@hypequery/protocol": "0.3.0"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"@hypequery/clickhouse": "*",
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"typescript": "^5.7.3",
|
|
37
38
|
"vitest": "^3.2.6",
|
|
38
39
|
"@hypequery/clickhouse": "2.4.0",
|
|
39
|
-
"@hypequery/serve": "0.
|
|
40
|
+
"@hypequery/serve": "0.13.0"
|
|
40
41
|
},
|
|
41
42
|
"repository": {
|
|
42
43
|
"type": "git",
|