acrylblends 1.2.0 → 1.3.1

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 (52) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +44 -10
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/guides/create-acrylblends.md +4 -2
  5. package/docs/framework/workshop/README.md +4 -2
  6. package/docs/framework/workshop/facilitator.md +1 -1
  7. package/lib/cordis/door.d.ts +54 -2
  8. package/lib/cordis/door.js +131 -4
  9. package/package.json +1 -1
  10. package/plugins/acryl-door/plugin.json +3 -0
  11. package/plugins/acryl-door/src/cli/acryl.ts +63 -6
  12. package/plugins/acryl-door/src/host/door.ts +91 -5
  13. package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
  14. package/plugins/acryl-door/src/host/verbs.ts +11 -1
  15. package/plugins/acryl-door/src/index.ts +2 -1
  16. package/plugins/builder/index.ts +13 -35
  17. package/plugins/builder/plugin.json +4 -2
  18. package/plugins/extensions/index.ts +78 -19
  19. package/plugins/extensions/plugin-store.ts +27 -0
  20. package/plugins/extensions/plugin.json +1 -1
  21. package/plugins/extensions/routes.ts +92 -0
  22. package/plugins/extensions/stage.ts +15 -5
  23. package/plugins/home/index.ts +3 -1
  24. package/plugins/manifest/plugin.json +1 -1
  25. package/plugins/plugin-share/README.md +66 -0
  26. package/plugins/plugin-share/index.ts +581 -0
  27. package/plugins/plugin-share/package.ts +186 -0
  28. package/plugins/plugin-share/plugin.json +15 -0
  29. package/plugins/plugin-share/publish.ts +65 -0
  30. package/plugins/plugin-share/pull-check.ts +129 -0
  31. package/plugins/plugin-share/pulled-records.ts +70 -0
  32. package/templates/agent/.gitattributes +2 -0
  33. package/templates/agent/blend.yaml +2 -0
  34. package/templates/agent/src/app.ts +6 -0
  35. package/templates/agent/template.json +1 -0
  36. package/templates/blank/.gitattributes +2 -0
  37. package/templates/blank/blend.yaml +3 -0
  38. package/templates/blank/src/app.ts +8 -0
  39. package/templates/blank/template.json +1 -0
  40. package/templates/chat/.gitattributes +2 -0
  41. package/templates/chat/blend.yaml +3 -0
  42. package/templates/chat/src/app.ts +8 -0
  43. package/templates/chat/template.json +1 -0
  44. package/templates/nodejs/.gitattributes +2 -0
  45. package/templates/nodejs/blend.yaml +3 -0
  46. package/templates/nodejs/src/app.ts +8 -0
  47. package/templates/nodejs/template.json +1 -0
  48. package/templates/notes/.gitattributes +2 -0
  49. package/templates/notes/blend.yaml +3 -0
  50. package/templates/notes/src/app.ts +8 -0
  51. package/templates/notes/template.json +1 -0
  52. package/templates/todo/.gitattributes +2 -0
package/dist/index.d.ts CHANGED
@@ -187,7 +187,8 @@ export declare function assertTargetFree(target: string): void;
187
187
  export declare function copyTemplate(source: string, target: string, name: string): string[];
188
188
  export declare function readTemplatePlugins(templateDir: string): string[];
189
189
  /**
190
- * Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. A package the app lists as a development
190
+ * Add run-time dependencies to an app's package.json and sort the keys. Returns the names to install: those added, and those moved because a plugin asks for a newer
191
+ * version than the app lists (the plugin's new code needs it). A version the app lists that is the same or newer stays. A package the app lists as a development
191
192
  * dependency too is taken out of that list: npm lets the development entry win, so `npm install --omit=dev` would leave a run-time dependency out.
192
193
  */
193
194
  export declare function mergeDependencies(appDir: string, dependencies: Readonly<Record<string, string>>): string[];
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ var packages = {
74
74
  },
75
75
  "acryl-blends-frame": {
76
76
  "name": "blends-frame",
77
- "version": "0.1.0"
77
+ "version": "0.1.1"
78
78
  }
79
79
  };
80
80
  //#endregion
@@ -1002,6 +1002,18 @@ function applyIdeProfile(root, input) {
1002
1002
  //#region src/version.ts
1003
1003
  /** The version of this package. */
1004
1004
  const VERSION = createRequire(import.meta.url)("../package.json").version;
1005
+ /** Semver order for the release versions (numeric segments; a pre-release sorts before its release). */
1006
+ function compareVersions(a, b) {
1007
+ const pa = a.split(/[.-]/u).map((part) => Number.isNaN(Number(part)) ? part : Number(part));
1008
+ const pb = b.split(/[.-]/u).map((part) => Number.isNaN(Number(part)) ? part : Number(part));
1009
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
1010
+ const x = pa[i] ?? 0;
1011
+ const y = pb[i] ?? 0;
1012
+ if (x < y) return -1;
1013
+ if (x > y) return 1;
1014
+ }
1015
+ return 0;
1016
+ }
1005
1017
  /** The DSH release the frame's packages pin as peers, so an app's DSH pin is the same one. */
1006
1018
  const FRAME_DSH_VERSION = dshVersion;
1007
1019
  /** Every package the frame publishes, in the file's order. */
@@ -1151,6 +1163,11 @@ const HOST_PROVIDED_IDS = /* @__PURE__ */ new Set([
1151
1163
  "save",
1152
1164
  "acryl-door"
1153
1165
  ]);
1166
+ /**
1167
+ * Services the app's rows provide that the host's own rows call: the door and the frame reach `pluginShare`, and no host row provides that name. Left out of
1168
+ * the isolate list, so they find it; every other service stays in the app's realm.
1169
+ */
1170
+ const REACHED_FROM_HOST = /* @__PURE__ */ new Set(["pluginShare"]);
1154
1171
  /** The generated `frame/app-plugins.mjs`, by the row that mounts it. */
1155
1172
  const APP_PLUGINS_FILE = "frame/app-plugins.mjs";
1156
1173
  /**
@@ -1176,12 +1193,14 @@ function appPluginsRow(prefix) {
1176
1193
  " return path === base || path.startsWith(base + '/') ? path : base + (path.startsWith('/') ? '' : '/') + path",
1177
1194
  "}",
1178
1195
  "",
1179
- "/** The router as the app's rows see it: mounts land under the prefix, every other member is the host router's. */",
1196
+ "/** The router as the app's rows see it: mounts land under the prefix, and a plugin's own request (its install check) finds them there; every other member is the host router's. */",
1180
1197
  "function appRouter(router, prefix) {",
1181
1198
  " const route = (path, app) => router.route(prefixedAppRoute(prefix, path), app)",
1199
+ " const request = (input, ...rest) => router.request(typeof input === 'string' && input.startsWith('/') ? prefixedAppRoute(prefix, input) : input, ...rest)",
1182
1200
  " return new Proxy(router, {",
1183
1201
  " get(target, property) {",
1184
1202
  " if (property === 'route') return route",
1203
+ " if (property === 'request') return request",
1185
1204
  " const value = Reflect.get(target, property, target)",
1186
1205
  " return typeof value === 'function' ? value.bind(target) : value",
1187
1206
  " },",
@@ -1190,8 +1209,10 @@ function appPluginsRow(prefix) {
1190
1209
  "",
1191
1210
  "export async function apply(ctx, config) {",
1192
1211
  ` const prefix = typeof config?.appPrefix === 'string' ? config.appPrefix : ${JSON.stringify(prefix)}`,
1193
- " // The rows read ctx.router as the app's own router: the same paths as on their own server, under the frame's prefix.",
1194
- " const appScope = ctx.extend({ router: appRouter(ctx.router, prefix) })",
1212
+ " // The rows read ctx.router as the app's own router: the same paths as on their own server, under the frame's prefix. A getter, read by each plugin",
1213
+ " // from its own context: it gets the host router traced for that context, so a route it mounts is its own and goes when the plugin is removed",
1214
+ " // (a router taken once here would make every route this row's, served until the whole app reloads).",
1215
+ " const appScope = ctx.extend({ get router() { return appRouter(this.get('router'), prefix) } })",
1195
1216
  " const fibers = await composeInto(appScope, ctx.appInstance, { host: true })",
1196
1217
  " ctx.effect(() => () => { for (const fiber of [...fibers].reverse()) void fiber.dispose() }, 'app-plugins: dispose')",
1197
1218
  "}",
@@ -1229,7 +1250,7 @@ function providedServices(appDir) {
1229
1250
  }
1230
1251
  };
1231
1252
  for (const row of ownRowIds(appDir)) walk(join(appDir, "plugins", row));
1232
- return Object.fromEntries([...provided].sort().map((name) => [name, true]));
1253
+ return Object.fromEntries([...provided].filter((name) => !REACHED_FROM_HOST.has(name)).sort().map((name) => [name, true]));
1233
1254
  }
1234
1255
  /**
1235
1256
  * The frame rows for an app: its routes row when it carries `src/frame.ts`, and the composition row when its `src/app.ts` exports `composeInto`.
@@ -1662,8 +1683,19 @@ function readTemplatePlugins(templateDir) {
1662
1683
  const { plugins } = JSON.parse(readFileSync(file, "utf8"));
1663
1684
  return plugins ?? [];
1664
1685
  }
1686
+ /** A version a plugin's pin can be compared with: an exact one or a caret or tilde range. A tag, a path or a URL is the app's own choice. */
1687
+ function comparableVersion(spec) {
1688
+ return /^[\^~]?\d+\.\d+\.\d+(?:-[\w.]+)?$/u.test(spec) ? spec.replace(/^[\^~]/u, "") : void 0;
1689
+ }
1690
+ /** Whether a plugin's pin asks for more than what the app has: the plugin's code was written against that version. */
1691
+ function asksForNewer(wanted, current) {
1692
+ const a = comparableVersion(wanted);
1693
+ const b = comparableVersion(current);
1694
+ return a !== void 0 && b !== void 0 && compareVersions(a, b) > 0;
1695
+ }
1665
1696
  /**
1666
- * Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. A package the app lists as a development
1697
+ * Add run-time dependencies to an app's package.json and sort the keys. Returns the names to install: those added, and those moved because a plugin asks for a newer
1698
+ * version than the app lists (the plugin's new code needs it). A version the app lists that is the same or newer stays. A package the app lists as a development
1667
1699
  * dependency too is taken out of that list: npm lets the development entry win, so `npm install --omit=dev` would leave a run-time dependency out.
1668
1700
  */
1669
1701
  function mergeDependencies(appDir, dependencies) {
@@ -1671,14 +1703,16 @@ function mergeDependencies(appDir, dependencies) {
1671
1703
  const manifest = JSON.parse(readFileSync(file, "utf8"));
1672
1704
  const current = manifest.dependencies ?? {};
1673
1705
  const added = Object.keys(dependencies).filter((name) => !(name in current));
1706
+ const moved = Object.entries(dependencies).filter(([name, wanted]) => current[name] !== void 0 && asksForNewer(wanted, current[name]));
1674
1707
  const merged = {
1675
1708
  ...dependencies,
1676
- ...current
1709
+ ...current,
1710
+ ...Object.fromEntries(moved)
1677
1711
  };
1678
1712
  manifest.dependencies = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
1679
1713
  if (manifest.devDependencies !== void 0) manifest.devDependencies = Object.fromEntries(Object.entries(manifest.devDependencies).filter(([name]) => !(name in merged)));
1680
1714
  writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
1681
- return added;
1715
+ return [...added, ...moved.map(([name]) => name)];
1682
1716
  }
1683
1717
  /** Add `package.json` "imports" entries (`#acryl-kv`: `./plugins/acryl-kv/src/index.ts`); an entry the app already has is kept. Returns the names added. */
1684
1718
  function mergeImports(appDir, imports, pluginsDir) {
@@ -2142,7 +2176,7 @@ async function add(options, io) {
2142
2176
  if (refused !== 0) return refused;
2143
2177
  }
2144
2178
  io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(", ")} to ${appDir}\n`);
2145
- if (addedDependencies.length > 0) io.stdout.write(`new dependencies: ${addedDependencies.join(", ")} (run npm install)\n`);
2179
+ if (addedDependencies.length > 0) io.stdout.write(`dependencies to install: ${addedDependencies.join(", ")} (run npm install)\n`);
2146
2180
  for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {
2147
2181
  const mounted = MOUNTED.find((candidate) => candidate.name === plugin.name);
2148
2182
  const result = mounted ? mount(appDir, mounted) : void 0;
@@ -3765,7 +3799,7 @@ async function upgrade(options, io) {
3765
3799
  const upgradedPlugins = plans.filter((p) => [...p.files.values()].some((s) => s !== "same" && s !== "extra") || p.isNew).map((p) => p.plugin.name);
3766
3800
  io.stdout.write(`\nupgraded ${[...upgradedPlugins, ...frameChanged ? ["the frame"] : []].join(", ")}\n`);
3767
3801
  if (conflicts + frameBackups > 0) io.stdout.write(`the ${conflicts + frameBackups} edited file(s) replaced by --force are saved in ${posix$1(relative(appDir, backup))}/\n`);
3768
- io.stdout.write(`acrylblends is now ^${VERSION} in package.json${addedDependencies.length > 0 ? `; new dependencies: ${[...new Set(addedDependencies)].join(", ")}` : ""}: run npm install, then npm run check and npm test\n`);
3802
+ io.stdout.write(`acrylblends is now ^${VERSION} in package.json${addedDependencies.length > 0 ? `; dependencies to install: ${[...new Set(addedDependencies)].join(", ")}` : ""}: run npm install, then npm run check and npm test\n`);
3769
3803
  return 0;
3770
3804
  } finally {
3771
3805
  rmSync(scratch, {