@sanoma-ai/workflows 0.9.0 → 0.10.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.
Files changed (205) hide show
  1. package/README.md +169 -52
  2. package/dist/apply-steps.js +5 -2
  3. package/dist/apply-steps.js.map +1 -1
  4. package/dist/apply.d.ts +3 -0
  5. package/dist/apply.js +35 -21
  6. package/dist/apply.js.map +1 -1
  7. package/dist/approvals.d.ts +1 -0
  8. package/dist/approvals.js +6 -1
  9. package/dist/approvals.js.map +1 -1
  10. package/dist/apps.d.ts +7 -24
  11. package/dist/apps.js +51 -67
  12. package/dist/apps.js.map +1 -1
  13. package/dist/ast.d.ts +9 -7
  14. package/dist/ast.js +2 -0
  15. package/dist/ast.js.map +1 -1
  16. package/dist/authentication.d.ts +24 -0
  17. package/dist/authentication.js +68 -0
  18. package/dist/authentication.js.map +1 -0
  19. package/dist/call-workflow.d.ts +5 -0
  20. package/dist/call-workflow.js +4 -3
  21. package/dist/call-workflow.js.map +1 -1
  22. package/dist/call.d.ts +3 -2
  23. package/dist/call.js +34 -7
  24. package/dist/call.js.map +1 -1
  25. package/dist/cedar.js +14 -1
  26. package/dist/cedar.js.map +1 -1
  27. package/dist/change-steps.js +7 -5
  28. package/dist/change-steps.js.map +1 -1
  29. package/dist/change.d.ts +7 -0
  30. package/dist/change.js +7 -0
  31. package/dist/change.js.map +1 -1
  32. package/dist/client.d.ts +10 -6
  33. package/dist/client.js +96 -43
  34. package/dist/client.js.map +1 -1
  35. package/dist/collection.d.ts +7 -0
  36. package/dist/collection.js +5 -2
  37. package/dist/collection.js.map +1 -1
  38. package/dist/config.d.ts +14 -3
  39. package/dist/config.js +55 -14
  40. package/dist/config.js.map +1 -1
  41. package/dist/connection.d.ts +168 -0
  42. package/dist/connection.js +327 -0
  43. package/dist/connection.js.map +1 -0
  44. package/dist/credentials.d.ts +7 -53
  45. package/dist/credentials.js +84 -247
  46. package/dist/credentials.js.map +1 -1
  47. package/dist/cucumber.d.ts +4 -2
  48. package/dist/cucumber.js +8 -3
  49. package/dist/cucumber.js.map +1 -1
  50. package/dist/db.js +21 -2
  51. package/dist/db.js.map +1 -1
  52. package/dist/define.d.ts +9 -2
  53. package/dist/define.js.map +1 -1
  54. package/dist/definitions.d.ts +20 -0
  55. package/dist/definitions.js +16 -0
  56. package/dist/definitions.js.map +1 -0
  57. package/dist/deployment-key.d.ts +2 -0
  58. package/dist/deployment-key.js +22 -0
  59. package/dist/deployment-key.js.map +1 -0
  60. package/dist/describe.d.ts +9 -177
  61. package/dist/describe.js +71 -34
  62. package/dist/describe.js.map +1 -1
  63. package/dist/description.d.ts +532 -0
  64. package/dist/description.js +381 -0
  65. package/dist/description.js.map +1 -0
  66. package/dist/direct.js +3 -0
  67. package/dist/direct.js.map +1 -1
  68. package/dist/drift.d.ts +1 -1
  69. package/dist/drift.js +1 -1
  70. package/dist/drift.js.map +1 -1
  71. package/dist/env.d.ts +2 -0
  72. package/dist/env.js +2 -2
  73. package/dist/env.js.map +1 -1
  74. package/dist/index.d.ts +10 -2
  75. package/dist/index.js +10 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/ledger.d.ts +2 -0
  78. package/dist/ledger.js +1 -1
  79. package/dist/ledger.js.map +1 -1
  80. package/dist/lint.d.ts +0 -9
  81. package/dist/lint.js +1 -16
  82. package/dist/lint.js.map +1 -1
  83. package/dist/load.d.ts +74 -0
  84. package/dist/load.js +357 -0
  85. package/dist/load.js.map +1 -0
  86. package/dist/lockfile.d.ts +2 -0
  87. package/dist/lockfile.js +8 -3
  88. package/dist/lockfile.js.map +1 -1
  89. package/dist/op.d.ts +10 -1
  90. package/dist/op.js +1 -1
  91. package/dist/op.js.map +1 -1
  92. package/dist/outline.js +13 -4
  93. package/dist/outline.js.map +1 -1
  94. package/dist/paths.d.ts +18 -0
  95. package/dist/paths.js +21 -3
  96. package/dist/paths.js.map +1 -1
  97. package/dist/plugin.d.ts +0 -11
  98. package/dist/plugin.js +3 -18
  99. package/dist/plugin.js.map +1 -1
  100. package/dist/policy.d.ts +2 -0
  101. package/dist/policy.js.map +1 -1
  102. package/dist/resource.d.ts +27 -4
  103. package/dist/resource.js +49 -4
  104. package/dist/resource.js.map +1 -1
  105. package/dist/resources.d.ts +33 -76
  106. package/dist/resources.js +219 -227
  107. package/dist/resources.js.map +1 -1
  108. package/dist/run.d.ts +2 -0
  109. package/dist/run.js.map +1 -1
  110. package/dist/sandbox.d.ts +1 -1
  111. package/dist/sandbox.js +7 -3
  112. package/dist/sandbox.js.map +1 -1
  113. package/dist/scenario.d.ts +7 -0
  114. package/dist/scenario.js +33 -3
  115. package/dist/scenario.js.map +1 -1
  116. package/dist/scenarios.js +7 -1
  117. package/dist/scenarios.js.map +1 -1
  118. package/dist/schema.js +8 -0
  119. package/dist/schema.js.map +1 -1
  120. package/dist/secret-store-check.d.ts +11 -0
  121. package/dist/secret-store-check.js +76 -0
  122. package/dist/secret-store-check.js.map +1 -0
  123. package/dist/secret-store.d.ts +106 -0
  124. package/dist/secret-store.js +228 -0
  125. package/dist/secret-store.js.map +1 -0
  126. package/dist/shared.d.ts +17 -11
  127. package/dist/shared.js +34 -7
  128. package/dist/shared.js.map +1 -1
  129. package/dist/sites.d.ts +4 -93
  130. package/dist/sites.js +61 -84
  131. package/dist/sites.js.map +1 -1
  132. package/dist/source.d.ts +6 -0
  133. package/dist/source.js +13 -0
  134. package/dist/source.js.map +1 -1
  135. package/dist/steps.d.ts +3 -1
  136. package/dist/steps.js.map +1 -1
  137. package/dist/version.d.ts +2 -1
  138. package/dist/version.js +2 -1
  139. package/dist/version.js.map +1 -1
  140. package/dist/wakeup-driver.js +15 -6
  141. package/dist/wakeup-driver.js.map +1 -1
  142. package/dist/worker.js +12 -8
  143. package/dist/worker.js.map +1 -1
  144. package/dist/writer.d.ts +53 -14
  145. package/dist/writer.js +113 -43
  146. package/dist/writer.js.map +1 -1
  147. package/oxlint.json +0 -5
  148. package/package.json +6 -2
  149. package/src/apply-steps.ts +5 -2
  150. package/src/apply.ts +70 -29
  151. package/src/approvals.ts +7 -1
  152. package/src/apps.ts +54 -85
  153. package/src/ast.ts +6 -6
  154. package/src/authentication.ts +74 -0
  155. package/src/call-workflow.ts +5 -4
  156. package/src/call.ts +53 -9
  157. package/src/cedar.ts +14 -1
  158. package/src/change-steps.ts +7 -4
  159. package/src/change.ts +10 -0
  160. package/src/client.ts +108 -41
  161. package/src/collection.ts +10 -2
  162. package/src/config.ts +72 -13
  163. package/src/connection.ts +446 -0
  164. package/src/credentials.ts +94 -281
  165. package/src/cucumber.ts +16 -3
  166. package/src/db.ts +21 -2
  167. package/src/define.ts +7 -2
  168. package/src/definitions.ts +31 -0
  169. package/src/deployment-key.ts +23 -0
  170. package/src/describe.ts +102 -230
  171. package/src/description.ts +426 -0
  172. package/src/direct.ts +3 -0
  173. package/src/drift.ts +10 -2
  174. package/src/env.ts +4 -2
  175. package/src/index.ts +42 -1
  176. package/src/ledger.ts +3 -1
  177. package/src/lint.ts +1 -16
  178. package/src/load.ts +437 -0
  179. package/src/lockfile.ts +10 -3
  180. package/src/op.ts +10 -1
  181. package/src/outline.ts +18 -5
  182. package/src/paths.ts +24 -2
  183. package/src/plugin.ts +3 -24
  184. package/src/policy.ts +2 -0
  185. package/src/resource.ts +67 -6
  186. package/src/resources.ts +246 -301
  187. package/src/run.ts +2 -0
  188. package/src/sandbox.ts +11 -3
  189. package/src/scenario.ts +45 -3
  190. package/src/scenarios.ts +7 -1
  191. package/src/schema.ts +10 -0
  192. package/src/secret-store-check.ts +93 -0
  193. package/src/secret-store.ts +314 -0
  194. package/src/shared.ts +35 -10
  195. package/src/sites.ts +70 -180
  196. package/src/source.ts +14 -0
  197. package/src/steps.ts +3 -1
  198. package/src/version.ts +2 -1
  199. package/src/wakeup-driver.ts +22 -6
  200. package/src/worker.ts +12 -8
  201. package/src/writer.ts +145 -56
  202. package/dist/datafile.d.ts +0 -97
  203. package/dist/datafile.js +0 -479
  204. package/dist/datafile.js.map +0 -1
  205. package/src/datafile.ts +0 -601
package/dist/resources.js CHANGED
@@ -1,29 +1,17 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { dirname, relative, resolve, sep } from "node:path";
3
- import { parsed } from "./ast.js";
4
- import { readDataFile, Reference, SECRET_ENTRIES, } from "./datafile.js";
1
+ import { readFileSync } from "node:fs";
2
+ import { mayDeclarePeople } from "./change.js";
3
+ import { isCollection } from "./collection.js";
4
+ import { filesRead, loadModules, madeOf, madeWith } from "./load.js";
5
5
  import { LOCKFILE, readLockfile } from "./lockfile.js";
6
- import { VENDOR } from "./op.js";
7
- import { APPS_DIR, filesUnder, kindDirs } from "./paths.js";
6
+ import { APPS_DIR, COLLECTIONS_FILE, filesUnder, kindDirs, relPath } from "./paths.js";
8
7
  import { peopleConnector, sanoma } from "./person.js";
9
8
  import { wakeupOps } from "./wakeup-type.js";
10
- import { resourceTypesOf } from "./resource.js";
9
+ import { DECLARED, isDeclared, isObject, resourceTypesOf } from "./resource.js";
11
10
  import { Principal } from "./define.js";
12
11
  import { NotAdmittedError } from "./errors.js";
13
- import { AGENTS, ANONYMOUS, ANONYMOUS_PRINCIPAL, declaresPeople, isVisitor, personOf, principalOf, problemLine, qualified, RUNTIME_AGENTS, VISITORS, } from "./shared.js";
14
- /** A file that imports the runtime's constructors, `sanoma` (by any local name): the files `readPeople` reads. */
15
- export const DECLARES_PEOPLE = /\{[^}]*\bsanoma\b[^}]*\}\s*from\s*["'](?:sanoma|@sanoma-ai\/workflows)\/resources["']/;
16
- /** The last read of the people under each root, by what its data files were then (`filesStamp`). */
17
- const peopleRead = new Map();
18
- /** Each data file under `root` with its size and modification time: unchanged while the files are. */
19
- const filesStamp = (root) => kindDirs(root, "resources")
20
- .flatMap(dataFilesUnder)
21
- .map((path) => {
22
- // Gone since it was listed (an editor's save by rename): stamped as such, and read next time.
23
- const stat = statSync(path, { bigint: true, throwIfNoEntry: false });
24
- return `${path} ${stat?.size} ${stat?.mtimeNs}`;
25
- })
26
- .join("\n");
12
+ import { AGENTS, ANONYMOUS, ANONYMOUS_PRINCIPAL, declaresPeople, isVisitor, personOf, principalOf, problemLine, RUNTIME_AGENTS, VISITORS, } from "./shared.js";
13
+ /** The people each load of the people's files declares: the same answer while none of them changed. */
14
+ const peopleRead = new WeakMap();
27
15
  /** The names no data file may give a group: what each is already. */
28
16
  const RESERVED_GROUPS = new Map([
29
17
  [AGENTS, "the group every agent is in"],
@@ -35,28 +23,38 @@ const RESERVED_GROUPS = new Map([
35
23
  * company's, so neither the policy (`readPeople`) nor `describeConfig` counts it.
36
24
  */
37
25
  export const inAppPeople = (r) => r.vendor === "sanoma" && Object.hasOwn(sanoma, r.type) && r.file.startsWith(`${APPS_DIR}/`);
26
+ const PEOPLE_CONNECTORS = [peopleConnector, wakeupOps];
27
+ const PEOPLE_TYPES = resourceTypesOf(PEOPLE_CONNECTORS);
28
+ const peopleLoad = (root) => loadModules(root, dataFilesOf(root), PEOPLE_CONNECTORS, { only: mayDeclarePeople, types: PEOPLE_TYPES });
29
+ /**
30
+ * The company files the people under `root` are read from (`readPeople`), by path relative to the
31
+ * root: each file that may declare them and every company file it imports, so an edit to a helper
32
+ * a people file reads its emails from is a change to the rules too.
33
+ */
34
+ export const peopleSources = (root) => filesRead(peopleLoad(root));
38
35
  /**
39
36
  * The people, agents and groups the data files under `root` declare (`sanoma.person`, `.agent`,
40
- * `.group`), read without running them, as `principalOf` takes them, with what is wrong in their
41
- * files; none without a root. Read again only when a data file changed (by its size and time),
42
- * so a new colleague is admitted once their file is saved, and a request costs a `stat` per file.
37
+ * `.group`), as `principalOf` takes them, with what is wrong in their files; none without a root.
38
+ * Only the files that may declare them (`mayDeclarePeople`) are loaded, with the people's and the
39
+ * wakeup's constructors, which the same `sanoma` declares; loaded again only when one of them
40
+ * changed (by its size and time), so a new colleague is admitted once their file is saved, and a
41
+ * request costs a `stat` per file.
43
42
  */
44
43
  export function readPeople(root) {
45
44
  if (root === undefined)
46
45
  return { people: [], problems: [] };
47
- const stamp = filesStamp(root);
48
- const cached = peopleRead.get(root);
49
- if (cached?.stamp === stamp)
50
- return cached.read;
51
- // With the wakeup's type, which the same constructors declare: read, and only the people's kept.
52
- const read = readDataFiles(root, [peopleConnector, wakeupOps], undefined, (source) => DECLARES_PEOPLE.test(source));
46
+ const loaded = peopleLoad(root);
47
+ const cached = peopleRead.get(loaded);
48
+ if (cached)
49
+ return cached;
50
+ // The people are never bound, so the lockfile is not read.
51
+ const read = declarationsOf(root, loaded, PEOPLE_TYPES, { bindings: {}, problems: [] }, true);
53
52
  const ofPeople = read.resources.filter((r) => Object.hasOwn(sanoma, r.type));
54
53
  const people = ofPeople.filter((r) => !inAppPeople(r));
55
- // The people are never bound, so the lockfile's problems are not theirs.
56
- const own = read.problems.filter((p) => p.file !== LOCKFILE);
54
+ const own = read.problems;
57
55
  for (const r of ofPeople.filter(inAppPeople)) {
58
56
  own.push({
59
- file: r.file,
57
+ ...r.at,
60
58
  message: `${r.id} declares a ${r.type} in an app: the people are the company's, declared in resources/ at the root`,
61
59
  });
62
60
  }
@@ -65,14 +63,14 @@ export function readPeople(root) {
65
63
  if (r.type === "group") {
66
64
  const reserved = RESERVED_GROUPS.get(r.name);
67
65
  if (reserved)
68
- own.push({ file: r.file, message: `${r.id} is named "${r.name}", ${reserved}: give it another name` });
66
+ own.push({ ...r.at, message: `${r.id} is named "${r.name}", ${reserved}: give it another name` });
69
67
  continue;
70
68
  }
71
69
  const id = r.name;
72
70
  const other = ids.get(id);
73
71
  if (other || RUNTIME_AGENTS.includes(id) || isVisitor({ id })) {
74
72
  own.push({
75
- file: r.file,
73
+ ...r.at,
76
74
  message: other
77
75
  ? `${r.id} has the id "${id}", which ${other.id} has: give each person and agent their own`
78
76
  : isVisitor({ id })
@@ -83,7 +81,7 @@ export function readPeople(root) {
83
81
  ids.set(id, r);
84
82
  }
85
83
  const declared = { people, problems: own };
86
- peopleRead.set(root, { stamp, read: declared });
84
+ peopleRead.set(loaded, declared);
87
85
  return declared;
88
86
  }
89
87
  /**
@@ -123,253 +121,247 @@ export function admit(asking, { people, problems }, signIn = false) {
123
121
  throw new NotAdmittedError(who);
124
122
  }
125
123
  /**
126
- * Reads `resources/` under `root`, the config's directory: each `.ts` file in it but
127
- * `*.test.ts`, `*.d.ts` and `node_modules`, parsed with oxc and held to the data-file subset
128
- * (`readDataFile`). A constructor import (`@sanoma-ai/connector-github/resources`) is matched to the
129
- * connector whose `info.package` it names, never imported; each resource is checked by calling
130
- * its type, as running the file would, with each resource it names given as that resource.
124
+ * Loads `resources/` under `root`, the config's directory, and each app's: each `.ts` file in it
125
+ * but `*.test.ts`, `*.d.ts` and `node_modules` (`loadModules`), its constructor imports (a
126
+ * connector's `<info.package>/resources`, `sanoma/resources`) served the constructors of
127
+ * `types`. Each export whose value a constructor made is a declaration, `<file>#<export>`, in the
128
+ * file that made it (its non-default name when the file gives two); each resource it names is the
129
+ * declaration that holds that very object.
131
130
  *
132
- * Problems: anything outside the subset, a constructor or data file the config does not know, a
133
- * type the connector does not declare, a value the type refuses (a reference where its type
134
- * takes none among them), a reference cycle, and two resources of one type with one identity.
131
+ * Problems: a file that throws as it loads (a value its type refuses, a type its vendor does not
132
+ * declare, an import of what the config does not serve), at the place its error's stack gives; a
133
+ * resource made but not exported, or exported under two names; one naming a resource no data file
134
+ * exports; a type that is not the config's; and two resources of one type with one identity (and
135
+ * connection). A declaration with a problem is left out, and so is any that names one.
135
136
  *
136
- * The collections `resources/data.ts` declares are listed as they are read (`collections`),
137
- * never matched to a connector, a resource type or the lockfile: the runtime imports that file.
137
+ * The collections `resources/data.ts` declares (`collection()`) are listed by their exports
138
+ * (`collections`), matched to no connector, resource type or lockfile: the runtime imports that
139
+ * file. A collection in another file, or anything else exported from `data.ts`, is a problem.
138
140
  *
139
141
  * Each declaration is joined to its binding in the lockfile (`readLockfile`) by its key,
140
- * `<file>#<export>`; a binding no export declares is an orphan. Problems: the lockfile's, a
141
- * binding of another type than its declaration's, and two declarations bound to one object.
142
- *
143
- * `partial` reads part of the files: only those whose source it passes, and in them, an import of
144
- * a connector not in `connectors`, or of a file left out, is left unread rather than reported.
142
+ * `<file>#<export>`; a binding no export declares is an orphan, unless its file failed to load.
143
+ * Problems: the lockfile's, a binding of another type than its declaration's, two declarations
144
+ * bound to one object, and several wakeups.
145
145
  */
146
- export function readDataFiles(root, connectors, types = resourceTypesOf(connectors), partial) {
147
- const problems = [];
146
+ export function loadDataFiles(root, connectors, types = resourceTypesOf(connectors)) {
148
147
  if (root === undefined) {
149
- problems.push({
150
- message: "The config has no root, so its data files cannot be found: make it with defineConfig, which records its file, or set its `root`",
151
- });
152
- return { resources: [], bindings: {}, orphans: [], collections: [], problems };
148
+ const message = "The config has no root, so its data files cannot be found: make it with defineConfig, which records its file, or set its `root`";
149
+ return { resources: [], bindings: {}, orphans: [], collections: [], problems: [{ message }] };
153
150
  }
154
- const lock = readLockfile(root);
155
- problems.push(...lock.problems);
156
- const files = new Map();
157
- const left = new Set();
158
- // The root's, then each app's: a data file imports only from its own `resources/`.
159
- for (const [dir, path] of kindDirs(root, "resources").flatMap((d) => dataFilesUnder(d).map((p) => [d, p]))) {
160
- const rel = relative(root, path).split(sep).join("/");
161
- const source = readFileSync(path, "utf8");
162
- if (partial && !partial(source)) {
163
- left.add(path);
151
+ const loaded = loadModules(root, dataFilesOf(root), connectors, { types });
152
+ return declarationsOf(root, loaded, types, readLockfile(root), false);
153
+ }
154
+ /** What the loaded data files declare, joined to the lockfile's bindings. */
155
+ function declarationsOf(root, loaded, types, lock, partial) {
156
+ const problems = [...lock.problems];
157
+ const failed = new Set();
158
+ const collectionsFiles = new Set(kindDirs(root, "resources").map((dir) => relPath(root, `${dir}/${COLLECTIONS_FILE}`)));
159
+ const COLLECTIONS_ONLY = "resources/data.ts holds collections only, since workflows and the runtime import it: declare a connector's resources in another data file";
160
+ // Each exported value, by identity, to the names it is exported under.
161
+ const names = new Map();
162
+ const exported = new Set();
163
+ const collections = [];
164
+ for (const [file, l] of loaded) {
165
+ if ("failedIn" in l) {
166
+ failed.add(file);
164
167
  continue;
165
168
  }
166
- const { program, problems: syntax, at } = parsed(source, path);
167
- const data = readDataFile(program, path, dir);
168
- const problem = (offset, message) => problems.push({ file: rel, ...at(offset), message });
169
- for (const p of syntax)
170
- problems.push({ file: rel, ...p });
171
- for (const p of data.problems)
172
- problem(p.start, p.message);
173
- files.set(path, { rel, data, problem });
174
- }
175
- // Each connector's constructors entry, `<info.package>/resources`, to its vendor.
176
- const entries = new Map();
177
- for (const connector of connectors) {
178
- const { id, info } = connector[VENDOR];
179
- if (info?.package)
180
- entries.set(`${info.package}/resources`, id);
181
- }
182
- // The runtime's own (`peopleConnector`, package `sanoma`) is imported from either name of its entry.
183
- if (entries.get("sanoma/resources") === "sanoma")
184
- for (const entry of SECRET_ENTRIES)
185
- entries.set(entry, "sanoma");
186
- // What each file's names stand for, and every resource read by its id.
187
- const vendors = new Map();
188
- const named = new Map();
189
- const declarations = new Map();
190
- for (const [path, file] of files) {
191
- const fileVendors = new Map();
192
- const fileNamed = new Map();
193
- for (const imp of file.data.imports) {
194
- if (imp.kind === "constructors") {
195
- const vendor = entries.get(imp.source);
196
- if (vendor === undefined && partial)
197
- continue;
198
- if (vendor === undefined) {
199
- file.problem(imp.start, `"${imp.source}" is not the resources entry of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``);
200
- }
201
- else if (imp.imported !== vendor) {
202
- file.problem(imp.start, `"${imp.source}" exports its constructors as ${vendor}: \`import { ${vendor} } from "${imp.source}"\``);
169
+ if ("error" in l) {
170
+ failed.add(file);
171
+ const message = loadProblem(l.error, root, file, partial);
172
+ if (message !== undefined)
173
+ problems.push({ file, ...(l.line !== undefined && { line: l.line, column: l.column ?? 1 }), message });
174
+ continue;
175
+ }
176
+ for (const [name, value] of l.exports) {
177
+ const at = { file, export: name };
178
+ if (isCollection(value)) {
179
+ if (!collectionsFiles.has(file)) {
180
+ problems.push({
181
+ ...at,
182
+ message: `a collection is declared in resources/data.ts, which workflows and the runtime import: move ${name} there`,
183
+ });
203
184
  }
204
185
  else {
205
- fileVendors.set(imp.local, vendor);
186
+ collections.push({ id: `${file}#${name}`, file, name: value.name, at });
206
187
  }
207
- continue;
208
188
  }
209
- const target = resolve(dirname(path), imp.source);
210
- const other = files.get(target);
211
- if (!other && left.has(target))
212
- continue;
213
- if (!other) {
214
- file.problem(imp.start, `"${imp.source}" is not a data file under resources/`);
215
- }
216
- else if (!other.data.declared.includes(imp.imported)) {
217
- file.problem(imp.start, `${imp.imported} is not a resource ${other.rel} declares`);
218
- }
219
- else if (other.data.exports.some((e) => e.name === imp.imported)) {
220
- fileNamed.set(imp.local, `${other.rel}#${imp.imported}`);
189
+ else if (isDeclared(value)) {
190
+ exported.add(value);
191
+ if (collectionsFiles.has(file))
192
+ problems.push({ ...at, message: COLLECTIONS_ONLY });
193
+ else
194
+ names.set(value, [...(names.get(value) ?? []), at]);
221
195
  }
222
- // Else its value was refused, and reported in its file: a reference to it fails quietly.
223
196
  }
224
- for (const exp of file.data.exports) {
225
- const id = `${file.rel}#${exp.name}`;
226
- fileNamed.set(exp.name, id);
227
- declarations.set(id, { file, exp });
197
+ }
198
+ // Each declaration: one name per object, a non-default one over `default`.
199
+ const declarations = new Map();
200
+ const idOf = new Map();
201
+ for (const [value, ats] of names) {
202
+ const named = ats.filter((at) => at.export !== "default");
203
+ const kept = named.length ? named : ats;
204
+ if (kept.length > 1) {
205
+ const [first, ...rest] = kept.map((at) => `${at.file}#${at.export}`);
206
+ problems.push({
207
+ ...kept[1],
208
+ message: `${rest.join(", ")} and ${first} are one ${value[DECLARED]}: export each resource under one name`,
209
+ });
210
+ continue;
211
+ }
212
+ const at = kept[0];
213
+ const type = types.get(value[DECLARED]);
214
+ if (type === undefined || madeOf(value) === undefined) {
215
+ if (!partial)
216
+ problems.push({
217
+ ...at,
218
+ message: `${value[DECLARED]} is not a resource type of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``,
219
+ });
220
+ continue;
228
221
  }
229
- vendors.set(file, fileVendors);
230
- named.set(file, fileNamed);
222
+ const id = `${at.file}#${at.export}`;
223
+ declarations.set(id, { value, at });
224
+ idOf.set(value, id);
231
225
  }
232
- // Each resource after the ones it names, which it is given as they are declared.
226
+ // A resource made and never exported is declared nowhere: a typo'd export, or one written inline.
227
+ for (const value of madeWith(loaded)) {
228
+ const made = madeOf(value);
229
+ if (!isDeclared(value) || exported.has(value) || failed.has(made.file))
230
+ continue;
231
+ problems.push({
232
+ file: made.file,
233
+ message: `${aResource(value)} is made here but not exported: declare each resource as its own \`export const <name> = <vendor>.<type>({ … })\``,
234
+ });
235
+ }
236
+ // Each declaration after the ones it names; one with a problem is left out, and so is any that names it.
233
237
  const done = new Map();
234
- const visiting = [];
235
238
  const byName = new Map();
239
+ const leaveOut = (id, message) => {
240
+ if (message !== undefined)
241
+ problems.push({ ...declarations.get(id).at, message });
242
+ done.set(id, undefined);
243
+ };
236
244
  function declare(id) {
237
245
  if (done.has(id))
238
246
  return done.get(id);
239
- const { file, exp } = declarations.get(id);
240
- if (visiting.includes(id)) {
241
- file.problem(exp.start, `${id} refers to itself: ${[...visiting.slice(visiting.indexOf(id)), id].join(" → ")}`);
242
- return undefined;
243
- }
244
- visiting.push(id);
245
- const result = build(file, exp);
246
- visiting.pop();
247
- if (result?.declared.name !== undefined) {
248
- const { vendor, type, name } = result.declared;
249
- const key = `${vendor}.${type} ${JSON.stringify(name)}`;
250
- const first = byName.get(key);
251
- if (first === undefined)
252
- byName.set(key, id);
253
- else {
254
- file.problem(exp.start, `${key} is declared twice, also as ${first}: declare each resource once, and import it where it is used`);
255
- done.set(id, undefined);
247
+ const { value } = declarations.get(id);
248
+ const refs = {};
249
+ for (const [path, target] of Object.entries(madeOf(value).refers)) {
250
+ const targetId = idOf.get(target);
251
+ // A partial load serves only some types: a resource of another (a wakeup's host) is not its to judge.
252
+ if (targetId === undefined && partial && !types.has(target[DECLARED]))
253
+ continue;
254
+ if (targetId === undefined) {
255
+ leaveOut(id, `${path} names ${aResource(target)} that no data file exports: export it from a data file and import it here`);
256
256
  return undefined;
257
257
  }
258
+ if (!declare(targetId))
259
+ return void done.set(id, undefined);
260
+ refs[path] = targetId;
258
261
  }
259
- done.set(id, result);
260
- return result;
261
- }
262
- function build(file, exp) {
263
- // An unknown constructors import was reported with the import.
264
- const vendor = vendors.get(file).get(exp.vendor);
265
- if (vendor === undefined)
266
- return undefined;
267
- const resource = types.get(`${vendor}.${exp.type}`);
268
- if (!resource) {
269
- const known = [...types.values()].filter((r) => r.vendor === vendor).map((r) => r.type);
270
- file.problem(exp.start, `${vendor} has no resource type ${exp.type}: its types are ${known.join(", ")}`);
271
- return undefined;
272
- }
273
- const refs = {};
274
- let ok = true;
275
- // The fields with each name of a resource as that resource, and where each is named.
276
- const given = (v, at) => {
277
- if (v instanceof Reference) {
278
- // A name that did not resolve was reported where it is bound.
279
- const target = named.get(file).get(v.name);
280
- const declared = target === undefined ? undefined : declare(target)?.declared;
281
- if (!declared)
282
- ok = false;
283
- else
284
- refs[at] = target;
285
- return declared;
262
+ if (value.name !== undefined) {
263
+ const key = `${value[DECLARED]} ${JSON.stringify(value.name)}${value.connection ? ` (${value.connection.$connection})` : ""}`;
264
+ const first = byName.get(key);
265
+ if (first !== undefined) {
266
+ leaveOut(id, `${key} is declared twice, also as ${first}: declare each resource once, and import it where it is used`);
267
+ return undefined;
286
268
  }
287
- if (Array.isArray(v))
288
- return v.map((item, i) => given(item, `${at}.${i}`));
289
- if (v === null || typeof v !== "object")
290
- return v;
291
- return Object.fromEntries(Object.entries(v).map(([k, item]) => [k, given(item, at ? `${at}.${k}` : k)]));
292
- };
293
- const fields = given(exp.desired, "");
294
- if (!ok)
295
- return undefined;
296
- try {
297
- // Checked as running the file would: by the resource type, which allows a resource only where its `fields.references` say.
298
- return { declared: resource(fields), refs };
299
- }
300
- catch (e) {
301
- // One line per problem: zod's own spans lines.
302
- file.problem(exp.start, `export const ${exp.name}: ${e.message.replaceAll(/\s*\n\s*/g, " ")}`);
303
- return undefined;
269
+ byName.set(key, id);
304
270
  }
271
+ done.set(id, refs);
272
+ return refs;
305
273
  }
306
274
  // Each binding against its declaration: of its type, and the only one bound to its object.
307
275
  const boundBy = new Map();
308
- for (const [id, { file, exp }] of declarations) {
276
+ for (const [id, { value }] of declarations) {
309
277
  const binding = lock.bindings[id];
310
- // An unknown constructors import was reported with the import.
311
- const vendor = vendors.get(file).get(exp.vendor);
312
- if (binding === undefined || vendor === undefined)
278
+ if (binding === undefined)
313
279
  continue;
314
- const type = `${vendor}.${exp.type}`;
315
- const object = `${binding.type} ${binding.id}`;
280
+ const object = `${binding.type} ${binding.id}${binding.connection ? ` (${binding.connection.$connection})` : ""}`;
316
281
  const first = boundBy.get(object);
317
- const why = binding.type !== type
318
- ? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${type}`
319
- : first !== undefined
320
- ? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
321
- : undefined;
282
+ const why = value.connection !== undefined && binding.connection?.$connection !== value.connection.$connection
283
+ ? `${id} changes the connection of a bound resource: explicitly unbind it before selecting another account`
284
+ : binding.type !== value[DECLARED]
285
+ ? `${id} is bound to a ${binding.type} in ${LOCKFILE}, but declares a ${value[DECLARED]}`
286
+ : first !== undefined
287
+ ? `${id} is bound to the same ${object} as ${first} in ${LOCKFILE}: bind each object once`
288
+ : undefined;
322
289
  if (why === undefined)
323
290
  boundBy.set(object, id);
324
- else {
325
- // Left out, as any declaration with a problem, and so is any that names it.
326
- file.problem(exp.start, why);
327
- done.set(id, undefined);
328
- }
291
+ else
292
+ leaveOut(id, why);
329
293
  }
330
294
  // An instance registers with one wakeup: several are each a problem, and left out, as is any that names one.
331
- const wakeups = [...declarations].filter(([, { file, exp }]) => types.get(`${vendors.get(file).get(exp.vendor)}.${exp.type}`)?.wakeup);
295
+ const wakeups = [...declarations.keys()].filter((id) => types.get(declarations.get(id).value[DECLARED])?.wakeup);
332
296
  if (wakeups.length > 1) {
333
- for (const [id, { file, exp }] of wakeups) {
334
- const others = wakeups.filter(([other]) => other !== id).map(([other]) => other);
335
- file.problem(exp.start, `an instance has one wakeup: leave out this one or ${others.join(", ")}`);
336
- done.set(id, undefined);
297
+ for (const id of wakeups) {
298
+ leaveOut(id, `an instance has one wakeup: leave out this one or ${wakeups.filter((other) => other !== id).join(", ")}`);
337
299
  }
338
300
  }
339
- const keys = new Set([...files.values()].flatMap((f) => f.data.declared.map((name) => `${f.rel}#${name}`)));
301
+ // A binding under a file that failed to load is no orphan: what it declares is not known.
340
302
  const orphans = Object.entries(lock.bindings)
341
- .filter(([key]) => !keys.has(key))
303
+ .filter(([key]) => !declarations.has(key) && !failed.has(key.slice(0, key.lastIndexOf("#"))))
342
304
  .map(([key, binding]) => ({ key, ...binding }));
343
305
  const resources = [];
344
- for (const [id, { file, exp }] of declarations) {
345
- const result = declare(id);
346
- if (!result)
306
+ for (const [id, { value, at }] of declarations) {
307
+ const refs = declare(id);
308
+ if (!refs)
347
309
  continue;
348
- const { vendor, type, name: identity, desired } = result.declared;
310
+ const { vendor, type, name: identity, desired, connection } = value;
349
311
  const binding = lock.bindings[id]?.id;
350
312
  resources.push({
351
313
  id,
352
- file: file.rel,
314
+ file: at.file,
353
315
  vendor,
354
316
  type,
355
- name: identity ?? exp.name,
317
+ name: identity ?? at.export,
356
318
  ...(identity !== undefined && { identity }),
357
319
  ...(binding !== undefined && { binding }),
358
- span: [exp.start, exp.end],
359
- desired,
360
- refs: result.refs,
320
+ at,
321
+ // A copy: the loaded value is the generation's, read again while no file changes.
322
+ desired: structuredClone(desired),
323
+ ...(connection && { connection: { ...connection } }),
324
+ refs,
361
325
  });
362
326
  }
363
- const collections = [...files.values()].flatMap(({ rel, data }) => data.collections.map((c) => ({
364
- id: `${rel}#${c.export}`,
365
- file: rel,
366
- // An app's, `apps/<app>/resources/data.ts`, is named with its app, as `collection()` names it.
367
- name: qualified(rel.startsWith(`${APPS_DIR}/`) ? rel.split("/")[1] : undefined, c.name),
368
- span: [c.start, c.end],
369
- })));
370
327
  problems.sort((a, b) => (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0));
371
328
  return { resources, bindings: lock.bindings, orphans, collections, problems };
372
329
  }
330
+ /** A resource as a problem names it: `a github.repository "website"`, or without its identity when it has none. */
331
+ const aResource = (value) => `a ${value[DECLARED]}${value.name === undefined ? "" : ` "${value.name}"`}`;
332
+ /**
333
+ * What a data file that failed to load is told, on one line: an import Node could not find (a
334
+ * connector the config does not have, a file that is not there), top-level `await`, or the
335
+ * error's own message (a constructor's refusal: `github.repository: no field wiki`). Nothing, when
336
+ * `partial`, for a connector's constructors left out of the load in a file that declares no
337
+ * person, agent or group (`sanoma.wakeup` beside its host's connector).
338
+ */
339
+ function loadProblem(error, root, file, partial) {
340
+ const { code, message } = isObject(error) ? error : {};
341
+ const text = typeof message === "string" ? message : String(error);
342
+ if (code === "ERR_REQUIRE_ASYNC_MODULE")
343
+ return "a data file is loaded synchronously: leave out top-level `await`, and compute values where they are used";
344
+ if (code === "ERR_MODULE_NOT_FOUND") {
345
+ const missing = /^Cannot find (?:package|module) '([^']+)'/.exec(text)?.[1];
346
+ const source = readFileSync(`${root}/${file}`, "utf8");
347
+ // The specifier as written: Node names a package by its name, a file by its path.
348
+ const written = missing &&
349
+ [...source.matchAll(/from\s*["']([^"']+)["']/g)]
350
+ .map((m) => m[1])
351
+ .find((spec) => spec === missing ||
352
+ spec.startsWith(`${missing}/`) ||
353
+ (missing.startsWith("/") && missing.endsWith(`/${spec.replace(/^(?:\.\.?\/)+/, "")}`)));
354
+ if (written?.endsWith("/resources") && partial && !/\.\s*(?:person|agent|group)\s*\(/.test(source))
355
+ return undefined;
356
+ if (written?.endsWith("/resources"))
357
+ return `"${written}" is not the resources entry of a connector in the config's \`connectors\`: add the connector, and import from its package's \`/resources\``;
358
+ if (written !== undefined)
359
+ return `"${written}" cannot be found: import a file that is there, or a package the company has`;
360
+ }
361
+ return text.replaceAll(/\s*\n\s*/g, " ");
362
+ }
363
+ /** The data files under `root`: the root's `resources/`, then each app's, each sorted. */
364
+ const dataFilesOf = (root) => kindDirs(root, "resources").flatMap(dataFilesUnder);
373
365
  /** The data files under `dir`, sorted: regular `.ts` files, less tests, declarations and `node_modules`. */
374
366
  const dataFilesUnder = (dir) => filesUnder(dir, (name) => name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts"));
375
367
  //# sourceMappingURL=resources.js.map