@context-use/open-sync 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-use/open-sync",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/open-sync.ts",
@@ -0,0 +1,143 @@
1
+ import { access, cp, mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { BunPlugin } from 'bun';
4
+
5
+ // This adapter deliberately supports one published layout. Review it when upgrading Connector.
6
+ const supportedVersion = '1.6.0';
7
+ const indexVersion = 1;
8
+ interface CatalogEntry {
9
+ file: string;
10
+ bytes: number;
11
+ provider: { service: string };
12
+ }
13
+
14
+ function unsupported(detail: string): never {
15
+ throw new Error(
16
+ `Unsupported Open Connector package: ${detail}. Review the Open Sync build adapter.`,
17
+ );
18
+ }
19
+
20
+ /** Parse only the pinned generator's format; never evaluate dependency code during preparation. */
21
+ function executorRegistry(source: string): Map<string, string> {
22
+ const lines = source.trim().split('\n');
23
+ if (
24
+ lines.shift() !== '/** Generated lazy imports for provider executors. Do not hand-edit. */' ||
25
+ lines.shift() !== 'export const executorModules = {' ||
26
+ lines.pop() !== '};'
27
+ ) {
28
+ unsupported('executor registry format changed');
29
+ }
30
+ const modules = new Map<string, string>();
31
+ for (const line of lines) {
32
+ const match =
33
+ /^ {4}(?:"([\w-]+)"|([\w]+)): \(\) => import\("(\.\/[\w-]+\/executors\.js)"\),$/.exec(line);
34
+ const service = match?.[1] ?? match?.[2];
35
+ const path = match?.[3];
36
+ if (!service || path !== `./${service}/executors.js` || modules.has(service)) {
37
+ unsupported('executor registry entry changed');
38
+ }
39
+ modules.set(service, path);
40
+ }
41
+ return modules;
42
+ }
43
+
44
+ function catalogEntries(value: unknown): CatalogEntry[] {
45
+ const index = value as { version?: unknown; providers?: CatalogEntry[] } | null;
46
+ if (index?.version !== indexVersion || !Array.isArray(index.providers)) {
47
+ unsupported('catalog index format changed');
48
+ }
49
+ const services = new Set<string>();
50
+ for (const entry of index.providers) {
51
+ const service = entry?.provider?.service;
52
+ if (
53
+ typeof service !== 'string' ||
54
+ !/^[\w-]+$/.test(service) ||
55
+ entry.file !== `${service}.json` ||
56
+ !Number.isInteger(entry.bytes) ||
57
+ entry.bytes < 0 ||
58
+ services.has(service)
59
+ ) {
60
+ unsupported('catalog index entry changed');
61
+ }
62
+ services.add(service);
63
+ }
64
+ return index.providers;
65
+ }
66
+
67
+ /** All knowledge of Connector's private package layout stays behind this build adapter. */
68
+ export async function prepareConnector(input: {
69
+ root: string;
70
+ providers: readonly string[];
71
+ assets: string;
72
+ }): Promise<BunPlugin> {
73
+ const root = await realpath(input.root);
74
+ const metadata = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
75
+ if (metadata.name !== '@oomol-lab/open-connector' || metadata.version !== supportedVersion) {
76
+ unsupported(
77
+ `expected @oomol-lab/open-connector ${supportedVersion}, found ${metadata.version}`,
78
+ );
79
+ }
80
+ const registry = join(root, 'src/providers/registry.generated.js');
81
+ const modules = executorRegistry(await readFile(registry, 'utf8'));
82
+ const sourceAssets = join(root, 'assets/open-connector');
83
+ const entries = catalogEntries(
84
+ JSON.parse(await readFile(join(sourceAssets, 'catalog/apps-index.json'), 'utf8')),
85
+ );
86
+ const selected = input.providers.map((service) => {
87
+ const entry = entries.find((item) => item.provider.service === service);
88
+ if (!entry || !modules.has(service)) {
89
+ throw new Error(
90
+ `Cannot bundle Open Connector provider ${JSON.stringify(service)}: catalog or executor is missing.`,
91
+ );
92
+ }
93
+ return entry;
94
+ });
95
+ await mkdir(join(input.assets, 'catalog/apps'), { recursive: true });
96
+ for (const entry of selected) {
97
+ await access(join(root, 'src/providers', modules.get(entry.provider.service)!));
98
+ const content = await readFile(join(sourceAssets, 'catalog/apps', entry.file));
99
+ if (
100
+ content.byteLength !== entry.bytes ||
101
+ JSON.parse(content.toString()).service !== entry.provider.service
102
+ ) {
103
+ unsupported(`catalog file does not match its index: ${entry.file}`);
104
+ }
105
+ await writeFile(join(input.assets, 'catalog/apps', entry.file), content);
106
+ }
107
+ if (!selected.length) {
108
+ // Bun embeds files, so retain the directory that Connector enumerates even with zero providers.
109
+ await writeFile(join(input.assets, 'catalog/apps/empty'), '');
110
+ }
111
+ await writeFile(
112
+ join(input.assets, 'catalog/apps-index.json'),
113
+ JSON.stringify({ version: indexVersion, providers: selected }),
114
+ );
115
+ await cp(join(sourceAssets, 'migrations'), join(input.assets, 'migrations'), { recursive: true });
116
+ const contents = `export const executorModules = {\n${selected
117
+ .map(
118
+ ({ provider }) =>
119
+ `${JSON.stringify(provider.service)}: () => import(${JSON.stringify(modules.get(provider.service))}),`,
120
+ )
121
+ .join('\n')}\n};`;
122
+ return {
123
+ name: 'open-sync-providers',
124
+ setup(build) {
125
+ let replaced = false;
126
+ build.onStart(() => {
127
+ replaced = false;
128
+ });
129
+ build.onLoad({ filter: /[/\\]providers[/\\]registry\.generated\.js$/ }, ({ path }) => {
130
+ if (path !== registry) {
131
+ return;
132
+ }
133
+ replaced = true;
134
+ return { contents, loader: 'js' };
135
+ });
136
+ build.onEnd((result) => {
137
+ if (result.success && !replaced) {
138
+ unsupported('the build did not load the expected executor registry');
139
+ }
140
+ });
141
+ },
142
+ };
143
+ }
package/src/build.ts CHANGED
@@ -1,6 +1,40 @@
1
- import { getConnectorAssetDirectory } from '@oomol-lab/open-connector';
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { prepareConnector } from './build/connector';
6
+ import type { SyncRegistration } from './models/definition';
2
7
 
3
- /** Options needed when a host embeds Open Sync in a Bun executable. */
4
- export function getOpenSyncBuildOptions() {
5
- return { assets: [getConnectorAssetDirectory()], external: ['proxy-agent'] };
8
+ /** Provider dependencies declared by registrations, without loading their sync executables. */
9
+ export function providersFromDefinitions(definitions: readonly SyncRegistration[]): string[] {
10
+ return [
11
+ ...new Set(
12
+ definitions.flatMap(({ definition }) =>
13
+ definition.provider ? [definition.provider.service] : [],
14
+ ),
15
+ ),
16
+ ].sort();
17
+ }
18
+
19
+ /**
20
+ * Prepare selected providers for a Bun executable. Pass plugins/external to Bun.build and assets
21
+ * to compile.assets. Await dispose() after the build (also on failure); it removes staged files.
22
+ * An empty provider list packages no providers. The installed dependency is never modified.
23
+ */
24
+ export async function getOpenSyncBuildOptions(options: { providers: readonly string[] }) {
25
+ const directory = await mkdtemp(join(tmpdir(), 'open-sync-build-'));
26
+ const dispose = () => rm(directory, { recursive: true, force: true });
27
+ try {
28
+ const entrypoint = fileURLToPath(import.meta.resolve('@oomol-lab/open-connector'));
29
+ const assets = join(directory, 'open-connector');
30
+ const plugin = await prepareConnector({
31
+ root: dirname(dirname(dirname(entrypoint))),
32
+ providers: [...new Set(options.providers)].sort(),
33
+ assets,
34
+ });
35
+ return { assets: [assets], external: ['proxy-agent'], plugins: [plugin], dispose };
36
+ } catch (error) {
37
+ await dispose();
38
+ throw error;
39
+ }
6
40
  }
package/src/open-sync.ts CHANGED
@@ -75,6 +75,21 @@ export async function createOpenSync(options: OpenSyncOptions): Promise<OpenSync
75
75
  runtimeToken,
76
76
  signal: lifetime.signal,
77
77
  });
78
+ const requiredProviders = new Set(
79
+ options.definitions.flatMap(({ definition }) =>
80
+ definition.provider ? [definition.provider.service] : [],
81
+ ),
82
+ );
83
+ if (requiredProviders.size) {
84
+ const available = new Set((await management.catalog()).map((entry) => entry.service));
85
+ for (const service of requiredProviders) {
86
+ if (!available.has(service)) {
87
+ throw new Error(
88
+ `Open Sync provider ${JSON.stringify(service)} is unavailable. Include it in getOpenSyncBuildOptions({ providers }) and rebuild.`,
89
+ );
90
+ }
91
+ }
92
+ }
78
93
  const client = createConnectorClient({
79
94
  fetch: transport,
80
95
  baseUrl: publicUrl,