@hypequery/cli 1.5.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 CHANGED
@@ -142,18 +142,19 @@ Builds deployment metadata for an exported HypeQuery API and writes the
142
142
  artifact with its identity sidecar.
143
143
 
144
144
  ```bash
145
- npx hypequery deployment:build analytics/api.ts \
146
- --runtime-artifact 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
145
+ npx hypequery deployment:build analytics/api.ts
147
146
  ```
148
147
 
149
- The runtime artifact digest is required when named Serve handlers cannot be
150
- lowered to a portable implementation. Dataset-only APIs can omit it.
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
151
 
152
152
  Options:
153
153
 
154
154
  - `--output <path>`: default `analytics/hypequery-deployment.json`
155
155
  - `--runtime <runtime>`: `node` (default) or `python`
156
- - `--runtime-artifact <sha256>`: lowercase SHA-256 of the built runtime artifact
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`
157
158
  - `--entrypoint-prefix <prefix>`: default `queries`
158
159
  - `--hash-output <path>`: default `<output>.sha256`
159
160
 
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;AAYpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAa9B,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;EAKpE;AAwJD,OAAO,EAAE,OAAO,EAAE,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
@@ -114,7 +114,8 @@ program
114
114
  .description('Build a validated, canonical deployment contract and identity')
115
115
  .option('-o, --output <path>', 'Output JSON file (default: analytics/hypequery-deployment.json)')
116
116
  .option('--runtime <runtime>', 'Runtime for non-portable handlers: node or python (default: node)')
117
- .option('--runtime-artifact <sha256>', 'SHA-256 identity of the built runtime artifact')
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)')
118
119
  .option('--entrypoint-prefix <prefix>', 'Runtime entrypoint prefix (default: queries)')
119
120
  .option('--hash-output <path>', 'Deployment identity sidecar path (default: <output>.sha256)')
120
121
  .action(runCommand(async (api, options) => {
@@ -3,6 +3,7 @@ export interface BuildDeploymentOptions {
3
3
  output?: string;
4
4
  runtime?: 'node' | 'python';
5
5
  runtimeArtifact?: string;
6
+ runtimeOutput?: string;
6
7
  entrypointPrefix?: string;
7
8
  hashOutput?: string;
8
9
  }
@@ -1 +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;AAM7B,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8BD,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAuCrC;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,OAAO,CAAC,0BAA0B,CAAC,CAoCrC"}
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"}
@@ -1,19 +1,23 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { prepareProtocolDeploymentContract, } from '@hypequery/protocol';
4
+ import { buildNodeRuntimeArtifact, getDeploymentRuntimeEntrypoints, } from '../utils/deployment-runtime-artifact.js';
4
5
  import { loadApiModule } from '../utils/load-api.js';
5
6
  import { logger } from '../utils/logger.js';
6
7
  const SHA256_PATTERN = /^[0-9a-f]{64}$/;
7
- function runtimeArtifact(options) {
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) {
8
16
  if (options.runtimeArtifact === undefined)
9
17
  return undefined;
10
18
  if (!SHA256_PATTERN.test(options.runtimeArtifact)) {
11
19
  throw new Error('--runtime-artifact must be a lowercase 64-character SHA-256 digest.');
12
20
  }
13
- const runtime = options.runtime ?? 'node';
14
- if (runtime !== 'node' && runtime !== 'python') {
15
- throw new Error('--runtime must be either node or python.');
16
- }
17
21
  return {
18
22
  runtime,
19
23
  artifactSha256: options.runtimeArtifact,
@@ -22,23 +26,72 @@ function runtimeArtifact(options) {
22
26
  : {}),
23
27
  };
24
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
+ }
25
42
  export async function buildDeploymentCommand(apiPath, options = {}) {
26
43
  if (!apiPath) {
27
44
  throw new Error('Missing API module path.\n\n'
28
45
  + 'Usage: hypequery deployment:build analytics/api.ts');
29
46
  }
30
- const artifact = runtimeArtifact(options);
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
+ }
31
61
  const api = await loadApiModule(apiPath);
32
62
  if (typeof api.deploymentContract !== 'function') {
33
63
  throw new Error(`Invalid API module: ${apiPath}\n\n`
34
64
  + 'The exported API must provide deploymentContract(). '
35
65
  + 'Upgrade @hypequery/serve and export the value returned by createAPI() or serve().');
36
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
+ }
37
84
  const contract = api.deploymentContract(artifact ? { runtimeArtifact: artifact } : {});
38
85
  const prepared = prepareProtocolDeploymentContract(contract);
39
86
  const { canonical, contract: validated, identity: digest } = prepared;
40
- const outputPath = options.output ?? 'analytics/hypequery-deployment.json';
41
- const hashOutputPath = options.hashOutput ?? `${outputPath}.sha256`;
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
+ });
42
95
  const identitySidecar = [
43
96
  '# Hypequery deployment identity v1; not a file checksum or sha256sum input.',
44
97
  '# SHA-256(UTF-8("hypequery:deployment:v1") || 0x00 || RFC 8785 canonical bytes); '
@@ -48,8 +101,16 @@ export async function buildDeploymentCommand(apiPath, options = {}) {
48
101
  ].join('\n');
49
102
  await mkdir(path.dirname(outputPath), { recursive: true });
50
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
+ }
51
108
  await writeFile(outputPath, `${canonical}\n`, 'utf8');
52
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
+ }
53
114
  logger.success(`Deployment contract written to ${outputPath}`);
54
115
  logger.info(`Identity: ${digest}`);
55
116
  return validated;
@@ -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
+ }
@@ -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"}
@@ -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.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Command-line interface for hypequery",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "typescript": "^5.7.3",
38
38
  "vitest": "^3.2.6",
39
39
  "@hypequery/clickhouse": "2.4.0",
40
- "@hypequery/serve": "0.12.0"
40
+ "@hypequery/serve": "0.13.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",