diff --git a/.github/workflows/build-integration-local.yml b/.github/workflows/build-integration-local.yml index dcfdf9c..ad14522 100644 --- a/.github/workflows/build-integration-local.yml +++ b/.github/workflows/build-integration-local.yml @@ -15,7 +15,7 @@ jobs: id-token: write contents: read steps: - - uses: Cohesible/get-credentials-action@d2795224e6f0ea7b2e41c4d3dbdb8bc3c873f450 + - uses: Cohesible/get-credentials-action@670287aebd309e1890507ab8ee7c8ed7eefa4c10 - uses: actions/checkout@v3 - run: curl -fsSL https://synap.sh/install | bash - run: synapse --version diff --git a/docs/getting-started.md b/docs/getting-started.md index 487d523..e3ecf9a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -173,6 +173,22 @@ synapse run -- alice # hello, alice! ``` +### `repl` + +Enters an interactive REPL session, optionally using a target file. The target file's exports are placed in the global scope. + +```main.ts +export function foo() { + return 'foo' +} +``` + +```shell +synapse repl main.ts +> foo() +# 'foo' +``` + ### `quote` Prints a motivational quote fetched from a public Synapse application. diff --git a/integrations/aws/src/services/api-gateway.ts b/integrations/aws/src/services/api-gateway.ts index 38f26f0..dff64d9 100644 --- a/integrations/aws/src/services/api-gateway.ts +++ b/integrations/aws/src/services/api-gateway.ts @@ -6,7 +6,7 @@ import { LambdaFunction } from './lambda' import { signRequest } from '../sigv4' import { NodeHttpHandler } from '@smithy/node-http-handler' import { HostedZone } from './route53' -import { HttpHandler, Middleware, RouteRegexp, buildRouteRegexp, matchRoutes, HttpResponse, HttpError, HttpRoute, PathArgs, createPathBindings, applyRoute, kHttpResponseBody, compareRoutes, HttpRequest } from 'synapse:http' +import { HttpHandler, Middleware, RouteRegexp, buildRouteRegexp, matchRoutes, HttpResponse, HttpError, HttpRoute, PathArgs, createPathBindings, applyRoute, compareRoutes, HttpRequest } from 'synapse:http' import { createSerializedPolicy } from './iam' import { generateIdentifier } from 'synapse:lib' import * as net from 'synapse:srl/net' @@ -307,9 +307,7 @@ async function runHandler(fn: () => Promise | T): Promise(response: http.ServerResponse, fn: () => Promise return await sendResponse(response, resp.body, resp.headers, resp.status) } - const body = kHttpResponseBody in resp - ? Buffer.from(resp[kHttpResponseBody] as Uint8Array) - : resp.body + const body = resp.body return await sendResponse(response, body, resp.headers, resp.status) } diff --git a/package.json b/package.json index 8c0243d..c64bc23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "synapse", - "version": "0.0.7", + "version": "0.0.8", "bin": "./src/cli/index.ts", "dependencies": { "esbuild": "^0.20.2", diff --git a/packages/resources.tgz b/packages/resources.tgz index feccbba..61c6a9a 100644 Binary files a/packages/resources.tgz and b/packages/resources.tgz differ diff --git a/src/cli/buildInternal.ts b/src/cli/buildInternal.ts index e91f341..1f88a8a 100644 --- a/src/cli/buildInternal.ts +++ b/src/cli/buildInternal.ts @@ -646,16 +646,22 @@ function stripComments(text: string) { return result.join('\n') } +export async function createSynapseTarball(dir: string) { + const files = await glob(getFs(), dir, ['**/*', '**/.synapse']) + const tarball = createTarball(await Promise.all(files.map(async f => ({ + contents: Buffer.from(await getFs().readFile(f)), + mode: 0o755, + path: path.relative(dir, f), + })))) + + const zipped = await gzip(tarball) + + return zipped +} + export async function createArchive(dir: string, dest: string, sign?: boolean) { if (path.extname(dest) === '.tgz') { - const files = await glob(getFs(), dir, ['**/*', '**/.synapse']) - const tarball = createTarball(await Promise.all(files.map(async f => ({ - contents: Buffer.from(await getFs().readFile(f)), - mode: 0o755, - path: path.relative(dir, f), - })))) - - const zipped = await gzip(tarball) + const zipped = await createSynapseTarball(dir) await getFs().writeFile(dest, zipped) } else if (path.extname(dest) === '.zip') { try { diff --git a/src/cli/commands.ts b/src/cli/commands.ts index 8e81f9f..eb48120 100644 --- a/src/cli/commands.ts +++ b/src/cli/commands.ts @@ -613,7 +613,7 @@ registerTypedCommand( hidden: true, options: [ { name: 'local', type: 'boolean' }, - { name: 'global', type: 'boolean', hidden: true }, + { name: 'remote', type: 'boolean', hidden: true }, { name: 'dry-run', type: 'boolean', hidden: true }, { name: 'skip-install', type: 'boolean', hidden: true }, { name: 'archive', type: 'string', hidden: true }, @@ -640,7 +640,7 @@ registerTypedCommand( options: [ { name: 'dry-run', type: 'boolean' } ], - }, + }, async (opt) => { await synapse.collectGarbage('', { ...opt, @@ -765,7 +765,7 @@ registerTypedCommand( 'import-identity', { internal: true, - args: [{ name: 'file', type: 'string' }], + args: [{ name: 'file', type: 'string', optional: true }], requirements: { program: false } }, async (target) => { @@ -962,11 +962,11 @@ registerTypedCommand( registerTypedCommand( 'repl', { - internal: true, // Temporary - args: [{ name: 'file', type: typescriptFileType }], + description: 'Enters an interactive REPL session, optionally using a target file. The target file\'s exports are placed in the global scope.', + args: [{ name: 'targetFile', type: typescriptFileType, optional: true }], options: buildTargetOptions, }, - (a, opt) => synapse.replCommand(a) + (a, opt) => synapse.replCommand(a, opt), ) registerTypedCommand( @@ -1407,7 +1407,7 @@ async function parseArgs(args: string[], desc: CommandDescriptor) { const minArgs = (desc.args?.filter(x => !x.allowMultiple && !x.optional).length ?? 0) + (allowMultipleArg?.minCount ?? 0) const providedArgs = parsedArgs.length + invalidPositionalArgs if (providedArgs < minArgs) { - for (let i = providedArgs; i < parsedArgs.length; i++) { + for (let i = providedArgs; i < minArgs; i++) { const a = desc.args![i] if (a.allowMultiple) break @@ -1425,7 +1425,7 @@ async function parseArgs(args: string[], desc: CommandDescriptor) { if (errors.length > 0) { throw new RenderableError('Invalid arguments', () => { for (const [n, e] of errors) { - printLine(colorize('brightRed', `${n} - ${e.message}`)) + printLine(colorize('brightRed', `${e.message} - ${n}`)) } }) } diff --git a/src/compiler/entrypoints.ts b/src/compiler/entrypoints.ts index a555825..cc7c548 100644 --- a/src/compiler/entrypoints.ts +++ b/src/compiler/entrypoints.ts @@ -185,7 +185,7 @@ function findInterestingSpecifiers(sf: ts.SourceFile, resolveBareSpecifier: (spe return { bare, zig } } -function createSpecifierResolver(cmd: ts.ParsedCommandLine, dir: string) { +function createSpecifierResolver(cmd: Pick, dir: string) { const baseUrl = cmd.options.baseUrl const paths = cmd.options.paths const resolveDir = baseUrl ?? dir diff --git a/src/compiler/resourceGraph.ts b/src/compiler/resourceGraph.ts index 30683be..642d344 100644 --- a/src/compiler/resourceGraph.ts +++ b/src/compiler/resourceGraph.ts @@ -23,7 +23,6 @@ interface SymbolNameComponents { interface ResourceInstantiation { readonly kind: string // FQN/Symbol - } export interface TypeInfo { diff --git a/src/index.ts b/src/index.ts index e66157f..1fd1d98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ import { createTemplateService, getHash, parseModuleName } from './templates' import { createImportMap, createModuleResolver } from './runtime/resolver' import { createAuth, getAuth } from './auth' import { generateOpenApiV3, generateStripeWebhooks } from './codegen/schemas' -import { addImplicitPackages, createMergedView, createNpmLikeCommandRunner, dumpPackage, emitPackageDist, getPkgExecutables, getProjectOverridesMapping, installToUserPath, linkPackage } from './pm/publish' +import { addImplicitPackages, createMergedView, createNpmLikeCommandRunner, dumpPackage, emitPackageDist, getPkgExecutables, getProjectOverridesMapping, installToUserPath, linkPackage, publishToRemote } from './pm/publish' import { ResolvedProgramConfig, getResolvedTsConfig, resolveProgramConfig } from './compiler/config' import { createProgramBuilder, getDeployables, getEntrypointsFile, getExecutables } from './compiler/programBuilder' import { loadCpuProfile } from './perf/profiles' @@ -62,6 +62,7 @@ import { createBlock, openBlock } from './build-fs/block' import { seaAssetPrefix } from './bundler' import { buildWindowsShim } from './zig/compile' import { openRemote } from './git' +import { getTypesFile } from './compiler/resourceGraph' export { runTask, getLogger } from './logging' @@ -111,7 +112,11 @@ export async function syncModule(deploymentId: string, bt = getBuildTargetOrThro } } -export async function publish(target: string, opt?: CompilerOptions & DeployOptions & { newFormat?: boolean; archive?: string; dryRun?: boolean; local?: boolean; globalInstall?: boolean; skipInstall?: boolean }) { +export async function publish(target: string, opt?: CompilerOptions & DeployOptions & { remote?: boolean; newFormat?: boolean; archive?: string; dryRun?: boolean; local?: boolean; skipInstall?: boolean }) { + if (opt?.remote) { + return publishToRemote(opt.archive) + } + if (opt?.archive) { const packageDir = getWorkingDir() const dest = path.resolve(packageDir, opt.archive) @@ -129,10 +134,11 @@ export async function publish(target: string, opt?: CompilerOptions & DeployOpti } if (opt?.local) { - await linkPackage({ dryRun: opt?.dryRun, globalInstall: opt?.globalInstall, skipInstall: opt?.skipInstall, useNewFormat: opt?.newFormat }) - } else { - throw new Error(`Publishing non-local packages is not implemented`) + await linkPackage({ dryRun: opt?.dryRun, skipInstall: opt?.skipInstall, useNewFormat: opt?.newFormat }) + return } + + throw new Error(`Publishing non-local packages is not implemented`) } async function findOrphans() { @@ -2391,11 +2397,19 @@ function normalizeToRelative(fileName: string, workingDir = getWorkingDir()) { return path.relative(workingDir, path.resolve(workingDir, fileName)) } -export async function replCommand(target: string, opt?: { entrypoint?: string; cwd?: string }) { +export async function replCommand(target?: string, opt?: {}) { + target = target ? normalizeToRelative(target) : target await compileIfNeeded(target) const repl = await runTask('', 'repl', async () => { + if (!target) { + const moduleLoader = await getModuleLoader(false) + + return enterRepl(undefined, moduleLoader, {}) + } + const files = await getEntrypointsFile() + const typesFile = await getTypesFile() const deployables = files?.deployables ?? {} const status = await validateTargetsForExecution(target, deployables) const outfile = status.sources?.[target]?.outfile @@ -2411,7 +2425,9 @@ export async function replCommand(target: string, opt?: { entrypoint?: string; c const moduleLoader = await runTask('init', 'loader', () => getModuleLoader(false), 1) // 8ms on simple hello world no infra - return enterRepl(resolved, moduleLoader, {}) + return enterRepl(resolved, moduleLoader, { + types: typesFile?.[target.replace(/\.tsx?$/, '.d.ts')], + }) }, 1) return repl.promise diff --git a/src/pm/packages.ts b/src/pm/packages.ts index 5becb1c..12bda79 100644 --- a/src/pm/packages.ts +++ b/src/pm/packages.ts @@ -31,6 +31,7 @@ import { cleanDir, fastCopyDir, removeDir } from '../zig/fs-ext' import { colorize, printLine } from '../cli/ui' import { OptimizedPackageManifest, PackageManifest, PublishedPackageJson, createManifestRepo, createMultiRegistryClient, createNpmRegistryClient } from './manifests' import { createGitHubPackageRepo, downloadGitHubPackage, githubPrefix } from './repos/github' +import { createSynapsePackageRepo, sprPrefix } from './repos/spr' // legacy const providerRegistryHostname = '' @@ -756,6 +757,7 @@ function createSprRepoWrapper( const fileRepo = createFilePackageRepo(fs, workingDirectory) const toolRepo = createToolRepo() const githubRepo = createGitHubPackageRepo() + const sprRepo = createSynapsePackageRepo() const _getTerraformPath = memoize(getTerraformPath) async function _getProviderVersions(name: string) { @@ -802,6 +804,10 @@ function createSprRepoWrapper( return _getProviderVersions(name.slice(providerPrefix.length)) } + if (name.startsWith(sprPrefix)) { + return sprRepo.listVersions(name.slice(sprPrefix.length)) + } + if (name.startsWith('cspm:')) { const manifest = await getPrivatePackageManifest(parseCspmRef(name)) @@ -835,6 +841,10 @@ function createSprRepoWrapper( } } + if (name.startsWith(sprPrefix)) { + return sprRepo.getPackageJson(name.slice(sprPrefix.length), version) + } + if (name.startsWith('cspm:')) { const manifest = await getPrivatePackageManifest(parseCspmRef(name)) const pkgJson = manifest.versions[version] @@ -888,6 +898,12 @@ function createSprRepoWrapper( return { name: `file:${override}`, version: parseVersionConstraint('*') } } + if (pattern.startsWith(sprPrefix)) { + const resolved = await sprRepo.resolvePattern(spec, pattern) + + return { name: `spr:${resolved.name}`, version: resolved.version } + } + return { name: pattern, version: parseVersionConstraint('*') } } @@ -934,6 +950,10 @@ function createSprRepoWrapper( return } + if (name.startsWith(sprPrefix)) { + return sprRepo.getDependencies(name.slice(sprPrefix.length), version) + } + if (name.startsWith('cspm:')) { const manifest = await getPrivatePackageManifest(parseCspmRef(name)) const inst = manifest.versions[version] @@ -964,6 +984,10 @@ function createSprRepoWrapper( return } + if (name.startsWith(sprPrefix)) { + return + } + if (name.startsWith('cspm:')) { // const manifest = await getPrivatePackageManifest(parseCspmRef(name)) // const inst = manifest.versions[version] diff --git a/src/pm/publish.ts b/src/pm/publish.ts index fcf74f2..f3e9703 100644 --- a/src/pm/publish.ts +++ b/src/pm/publish.ts @@ -2,20 +2,21 @@ import * as path from 'node:path' import { StdioOptions } from 'node:child_process' import { mergeBuilds, pruneBuild, consolidateBuild, commitPackages, getInstallation, writeSnapshotFile, getProgramFs, getDataRepository, getModuleMappings, loadSnapshot, dumpData, getProgramFsIndex, getDeploymentFsIndex, toFsFromIndex, copyFs, createSnapshot, getOverlayedFs, Snapshot, ReadonlyBuildFs } from '../artifacts' import { NpmPackageInfo, getDefaultPackageInstaller, installFromSnapshot, testResolveDeps } from './packages' -import { getBinDirectory, getSynapseDir, getLinkedPackagesDirectory, getToolsDirectory, getUserEnvFileName, getWorkingDir, listPackages, resolveProgramBuildTarget, SynapseConfiguration, getUserSynapseDirectory, setPackage, BuildTarget, findDeployment } from '../workspaces' -import { gzip, isNonNullable, keyedMemoize, linkBin, makeExecutable, memoize, throwIfNotFileNotFoundError, tryReadJson } from '../utils' +import { getBinDirectory, getSynapseDir, getLinkedPackagesDirectory, getToolsDirectory, getUserEnvFileName, getWorkingDir, listPackages, resolveProgramBuildTarget, SynapseConfiguration, getUserSynapseDirectory, setPackage, BuildTarget, findDeployment, getOrCreateRemotePackage } from '../workspaces' +import { gunzip, gzip, isNonNullable, keyedMemoize, linkBin, makeExecutable, memoize, throwIfNotFileNotFoundError, tryReadJson } from '../utils' import { Fs, ensureDir } from '../system' import { glob } from '../utils/glob' -import { createTarball, extractTarball } from '../utils/tar' import { getLogger, runTask } from '..' import { homedir } from 'node:os' import { getBuildTargetOrThrow, getFs, getSelfPathOrThrow, isSelfSea } from '../execution' import { ImportMap, expandImportMap, hoistImportMap } from '../runtime/importMaps' import { createCommandRunner, patchPath, runCommand } from '../utils/process' -import { PackageJson, ResolvedPackage, getImmediatePackageJsonOrThrow, getPackageJson } from './packageJson' +import { PackageJson, ResolvedPackage, getCompiledPkgJson, getCurrentPkg, getImmediatePackageJsonOrThrow, getPackageJson } from './packageJson' import { readKey, setKey } from '../cli/config' import { getEntrypointsFile } from '../compiler/programBuilder' -import { createPackageForRelease } from '../cli/buildInternal' +import { createPackageForRelease, createSynapseTarball } from '../cli/buildInternal' +import * as registry from '@cohesible/resources/registry' +import { extractTarball } from '../utils/tar' const getDependentsFilePath = () => path.resolve(getUserSynapseDirectory(), 'packageDependents.json') @@ -91,6 +92,57 @@ function getLinkedPkgPath(name: string, deploymentId?: string) { return path.resolve(packagesDir, deploymentId ? `${name}-${deploymentId}` : name) } +async function publishTarball(tarball: Buffer, pkgJson: PackageJson) { + if (!pkgJson.version) { + throw new Error('Package is missing a version') + } + + const remotePkgId = await getOrCreateRemotePackage() + const client = registry.createClient() + const { hash } = await client.uploadPackage(tarball) + await client.publishPackage({ + packageId: remotePkgId, + packageHash: hash, + packageJson: pkgJson, + }) +} + +async function publishTarballToRemote(tarballPath: string) { + const tarball = Buffer.from(await getFs().readFile(tarballPath)) + const files = extractTarball(await gunzip(tarball)) + const pkgJsonFile = files.find(f => f.path === 'package.json') + if (!pkgJsonFile) { + throw new Error(`Missing package.json inside tarball`) + } + + const pkgJson = JSON.parse(pkgJsonFile.contents.toString('utf-8')) + + await publishTarball(tarball, pkgJson) +} + +export async function publishToRemote(tarballPath?: string) { + if (tarballPath) { + return publishTarballToRemote(tarballPath) + } + + const bt = getBuildTargetOrThrow() + const packageDir = getWorkingDir() + const tmpDest = path.resolve(packageDir, `${path.dirname(packageDir)}-tmp`) + + const pkgJson = await getCurrentPkg() + if (!pkgJson) { + throw new Error('Missing package.json') + } + + try { + await createPackageForRelease(packageDir, tmpDest, { environmentName: bt.environmentName }, true, true) + const tarball = await createSynapseTarball(tmpDest) + await publishTarball(tarball, pkgJson.data) + } finally { + await getFs().deleteFile(tmpDest).catch(throwIfNotFileNotFoundError) + } +} + export async function linkPackage(opt?: PublishOptions & { globalInstall?: boolean; skipInstall?: boolean; useNewFormat?: boolean }) { const bt = getBuildTargetOrThrow() diff --git a/src/pm/repos/spr.ts b/src/pm/repos/spr.ts new file mode 100644 index 0000000..a9b52d8 --- /dev/null +++ b/src/pm/repos/spr.ts @@ -0,0 +1,83 @@ +import * as path from 'node:path' +import * as registry from '@cohesible/resources/registry' +import { ensureDir, gunzip, keyedMemoize, memoize } from '../../utils' +import { PackageJson, getRequired } from '../packageJson' +import { PackageRepository, ResolvePatternResult } from '../packages' +import { parseVersionConstraint } from '../versions' +import { PackageInfo } from '../../runtime/modules/serdes' +import { getFs } from '../../execution' +import { extractTarball, extractToDir, hasBsdTar } from '../../utils/tar' +import { listRemotePackages } from '../../workspaces' + +export const sprPrefix = 'spr:' + +const shouldUseRemote = !!process.env['SYNAPSE_SHOULD_USE_REMOTE'] +const getClient = memoize(() => registry.createClient(shouldUseRemote ? undefined : { authorization: () => 'none' })) + +export function createSynapsePackageRepo(): PackageRepository { + const getPrivatePackages = memoize(() => listRemotePackages()) + const getManifest = keyedMemoize((pkgId: string) => getClient().getPackageManifest(pkgId)) + + const getPackageJson = keyedMemoize(async (pkgId: string, version: string) => { + const manifest = await getManifest(pkgId) + const pkgJson = manifest.versions[version] + if (!pkgJson) { + throw new Error(`Version "${version}" not found for package: ${pkgId}`) + } + + return pkgJson + }) + + async function listVersions(pkgId: string) { + const manfiest = await getManifest(pkgId) + + return Object.keys(manfiest) + } + + async function resolvePattern(spec: string, pattern: string): Promise { + if (!pattern.startsWith('#')) { + throw new Error('Public packages not implemented') + } + + const pkgs = await getPrivatePackages() + const pkgId = pkgs?.[pattern.slice(1)] + if (!pkgId) { + throw new Error(`Package not found: ${pattern}`) + } + + return { + name: pkgId, + version: parseVersionConstraint('*'), + } + } + + async function getDependencies(pkgId: string, version: string) { + const pkg = await getPackageJson(pkgId, version) + + return getRequired(pkg) + } + + return { + listVersions, + resolvePattern, + getDependencies, + getPackageJson, + } +} + +export async function downloadSynapsePackage(info: PackageInfo, dest: string) { + const integrity = info.resolved?.integrity + if (!integrity?.startsWith('sha256:')) { + throw new Error(`Missing integrity for package: ${info.name} [destination: ${dest}]`) + } + + const publishedHash = integrity.slice('sha256:'.length) + const data = await getClient().downloadPackage(publishedHash, info.name) + + const tarball = await gunzip(data) + const files = extractTarball(tarball) + await Promise.all(files.map(async f => { + const absPath = path.resolve(dest, f.path) + await getFs().writeFile(absPath, f.contents, { mode: f.mode }) + })) +} diff --git a/src/repl.ts b/src/repl.ts index 991d4d2..e0e2171 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -10,9 +10,13 @@ import { SessionContext } from './deploy/deployment' import { pointerPrefix } from './build-fs/pointers' import { getDisplay } from './cli/ui' import { getArtifactFs } from './artifacts' +import { getBuildTargetOrThrow } from './execution' +import { TypeInfo } from './compiler/resourceGraph' +import { ensureDir } from './utils' export interface ReplOptions extends CombinedOptions { onInit?: (instance: ReplInstance) => void + types?: Record } interface ReplInstance { @@ -88,16 +92,30 @@ export async function createReplServer( } } +// XXX: a "proper" solution would involve rewriting all call expressions on the target +// to use property access expressions instead +// +// But there doesn't seem to be an easy way to transform the input besides wrapping the input +// stream or using a custom eval function. This is simpler albeit a little hacky. +// +// Copying all owned props onto the function might be an improvement +function wrapCallable(target: any, prop: PropertyKey) { + const fn = function (this: any, ...args: any[]) { + return Reflect.apply(target[prop], this ?? target, args) + } + + return Object.setPrototypeOf(fn, target) +} + async function createRepl( target: string | undefined, loader: ReturnType, options: ReplOptions, socket?: net.Socket ) { - const [targetModule, loggingModule] = await Promise.all([ - target ? loader.loadModule(target) : undefined, - undefined - ]) + const targetModule = target ? await loader.loadModule(target) : undefined + + const types = options.types const instance = repl.start({ useGlobal: true, @@ -109,8 +127,10 @@ async function createRepl( terminal: socket ? true : undefined, }) - // TODO: history should be per-program - const historyFile = path.resolve(getUserSynapseDirectory(), 'repl-history') + const historyDir = path.resolve(getUserSynapseDirectory(), 'repl-history') + await ensureDir(historyDir) + + const historyFile = path.resolve(historyDir, getBuildTargetOrThrow().programId) instance.setupHistory(historyFile, err => { if (err) { getLogger().error(`Failed to setup REPL history`, err) @@ -143,7 +163,12 @@ async function createRepl( for (const [k, v] of Object.entries(targetModule)) { if (k === '__esModule') continue - setValue(k, v) + const ty = types?.[k] + if (ty?.callable) { + setValue(k, wrapCallable(v, ty.callable)) + } else { + setValue(k, v) + } } } @@ -166,7 +191,7 @@ async function createRepl( export async function enterRepl( target: string | undefined, loader: ReturnType, - options: CombinedOptions + options: CombinedOptions & { types?: Record } ) { await getDisplay().releaseTty() const instance = await createRepl(target, loader, options) diff --git a/src/runtime/modules/http.ts b/src/runtime/modules/http.ts index 4171743..df374e8 100644 --- a/src/runtime/modules/http.ts +++ b/src/runtime/modules/http.ts @@ -27,11 +27,6 @@ type ExtractPattern = T extends `${infer P}{${infer U}}${infer export type CapturedPattern = string extends T ? Record : { [P in ExtractPattern]: string } type SplitRoute = T extends `${infer M extends HttpMethod} ${infer P}` ? [M, P] : [string, T] -// type X = ExtractPattern2<'/{foo}/{bar}'> -// type ExtractPattern2 = T extends `${infer P}{${infer U}}${infer S}` -// ? [...ExtractPattern2

, TrimRoute, ...ExtractPattern2] -// : [] - export type PathArgs = T extends `${infer P}{${infer U}}${infer S}` ? [...PathArgs

, string, ...PathArgs] : [] @@ -69,29 +64,6 @@ export type HttpHandler = (...args: [...PathArgs, ...(U extends undefined ? [] : [body: U])]) => Promise -// Only used to short-circuit APIG responses -/** @internal */ -export const kHttpResponseBody = Symbol.for('kHttpResponseBody') -// export class JsonResponse extends Response { -// readonly [kHttpResponseBody]: Uint8Array - -// public constructor(body: T, init?: ResponseInit | undefined) { -// const headers = new Headers(init?.headers) -// if (!headers.has('content-type')) { -// headers.set('content-type', 'application/json') -// } - -// const encoded = new TextEncoder().encode(JSON.stringify(body)) -// super(encoded, { ...init, headers }) - -// this[kHttpResponseBody] = encoded -// } -// } - -// export interface JsonResponse { -// json(): Promise -// } - interface BindingBase { from: string // JSONPath to: string // JSONPath diff --git a/src/runtime/srl/compute/index.ts b/src/runtime/srl/compute/index.ts index a223d88..6f2c8cc 100644 --- a/src/runtime/srl/compute/index.ts +++ b/src/runtime/srl/compute/index.ts @@ -85,6 +85,13 @@ export declare class HttpService { /** @internal */ forward(req: HttpRequest, body: any): Promise + //# resource = true + addRoute( + route: U, + handler: HttpHandler, + opt: never // XXX: extra arg to force TypeScript into the correct signature + ): HttpRoute, R> + //# resource = true addRoute

( route: P, diff --git a/src/workspaces.ts b/src/workspaces.ts index a47687f..88918bf 100644 --- a/src/workspaces.ts +++ b/src/workspaces.ts @@ -9,6 +9,7 @@ import { getHash, keyedMemoize, memoize, throwIfNotFileNotFoundError, tryReadJso import { glob } from './utils/glob' import { getBuildTarget, getBuildTargetOrThrow, getFs, isInContext } from './execution' import * as projects from '@cohesible/resources/projects' +import * as registry from '@cohesible/resources/registry' import { getPackageJson } from './pm/packageJson' import { randomUUID } from 'node:crypto' @@ -484,6 +485,7 @@ async function updateProjectState(state: ProjectState) { apps: state.apps, packages: state.packages, programs: state.programs, + importedPackages: state.importedPackages, }) } } @@ -724,6 +726,7 @@ interface ProjectState { readonly apps: Record readonly programs: Record readonly packages: Record // package name -> program id + readonly importedPackages?: Record } async function findProjectFromDir(dir: string) { @@ -775,8 +778,12 @@ async function createProject(rootDir: string, remotes?: Omit[]) { - const remote = shouldCreateRemoteProject && !process.env['SYNAPSE_FORCE_NO_REMOTE'] - ? await getOrCreateRemoteProject(dir, remotes) : undefined + const remote = !isRemoteDisabled() ? await getOrCreateRemoteProject(dir, remotes) : undefined const proj = { id: randomUUID(), kind: 'project', apps: remote?.apps, programs: remote?.programs, packages: remote?.packages, owner: '' } const ents = await getEntities() @@ -885,6 +891,32 @@ export async function listPackages(projectId?: string) { return state?.packages ?? {} } +export async function listRemotePackages(projectId?: string) { + projectId ??= await getCurrentProjectId() + const state = await getProjectState(projectId) + if (!state) { + return + } + + const packages = state?.packages ?? {} + const result: Record = {} + + for (const [k, v] of Object.entries(packages)) { + const imported = state.importedPackages?.[k] + if (imported) { + result[k] = imported + } + + const pkgId = getRemotePackageId(state, v) + if (!pkgId) continue + + // Overrides imported packages + result[k] = pkgId + } + + return result +} + export async function listDeployments(id?: string) { const projectId = id ?? await getCurrentProjectId() @@ -962,3 +994,68 @@ export async function isPublished(programId: string) { return false } + +function getRemotePackageId(state: ProjectState, programId: string) { + const bt = getBuildTargetOrThrow() + const appId = state.programs[programId]?.appId + if (!appId) { + return + // throw new Error(`No app id found for program: ${programId}`) + } + + const app = state.apps[appId] + if (!app) { + throw new Error(`Missing app: ${appId}`) + } + + const envName = bt.environmentName ?? app.defaultEnvironment ?? 'local' + const environment = app.environments[envName] + if (!environment) { + throw new Error(`Missing environment "${envName}" in app: ${appId}`) + } + + return environment.packageId +} + +export async function getOrCreateRemotePackage() { + const bt = getBuildTargetOrThrow() + const state = await getProjectState(bt.projectId) + if (!state) { + throw new Error(`No project state found: ${bt.projectId}`) + } + + const appId = state.programs[bt.programId]?.appId + if (!appId) { + throw new Error(`No app id found for program: ${bt.programId}`) + } + + const app = state.apps[appId] + if (!app) { + throw new Error(`Missing app: ${appId}`) + } + + const envName = bt.environmentName ?? app.defaultEnvironment ?? 'local' + const environment = app.environments[envName] + if (!environment) { + throw new Error(`Missing environment "${envName}" in app: ${appId}`) + } + + if (environment.packageId) { + return environment.packageId + } + + if (!shouldUseRemote) { + throw new Error(`Unable to create new remote package for app: ${app.id}`) + } + + getLogger().log('Creating new package for app', app.id) + const pkg = await registry.createClient().createPackage() + app.environments[envName] = { + ...environment, + packageId: pkg.id, + } + + await updateProjectState(state) + + return pkg.id +}