@reboot-dev/reboot 0.19.0 → 0.19.2

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,30 +1,35 @@
1
1
  {
2
2
  "dependencies": {
3
- "@types/node": "20.11.5",
4
- "@types/uuid": "^9.0.4",
5
- "uuid": "^9.0.1",
6
- "node-addon-api": "^7.0.0",
7
- "protobufjs": "^7.2.5",
8
- "chalk": "^4.1.2"
9
- },
10
- "peerDependencies": {
11
3
  "@bufbuild/protobuf": "1.3.2",
12
4
  "@bufbuild/protoplugin": "1.3.2",
13
5
  "@bufbuild/protoc-gen-es": "1.3.2",
14
- "@reboot-dev/reboot-api": "0.19.0",
15
- "typescript": ">=4.9.5"
6
+ "@reboot-dev/reboot-api": "0.19.2",
7
+ "chalk": "^4.1.2",
8
+ "node-addon-api": "^7.0.0",
9
+ "node-gyp": ">=10.2.0",
10
+ "uuid": "^9.0.1",
11
+ "which-pm-runs": "^1.1.0",
12
+ "extensionless": "^1.9.9",
13
+ "esbuild": "^0.24.0"
16
14
  },
17
15
  "type": "module",
18
16
  "name": "@reboot-dev/reboot",
19
- "version": "0.19.0",
17
+ "version": "0.19.2",
20
18
  "description": "npm package for Reboot",
21
19
  "scripts": {
22
20
  "postinstall": "rbt || exit 0",
23
21
  "test": "echo \"Error: no test specified\" && exit 1",
24
- "prepare": "tsc"
22
+ "prepack": "tsc"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": ">=4.9.5",
26
+ "@types/node": "20.11.5",
27
+ "@types/uuid": "^9.0.4"
25
28
  },
26
29
  "bin": {
27
- "rbt": "./rbt.js"
30
+ "rbt": "./rbt.js",
31
+ "rbt-esbuild": "./rbt-esbuild.js",
32
+ "protoc-gen-es": "./protoc-gen-es.js"
28
33
  },
29
34
  "engines": {
30
35
  "node": ">=18.0.0"
@@ -34,22 +39,27 @@
34
39
  "author": "reboot-dev",
35
40
  "license": "Apache-2.0",
36
41
  "files": [
42
+ "binding.gyp",
37
43
  "include",
38
44
  "internal",
39
- "utils",
40
- "binding.gyp",
41
45
  "package.json",
46
+ "protoc-gen-es.d.ts",
47
+ "protoc-gen-es.js",
48
+ "rbt.d.ts",
49
+ "rbt-esbuild.d.ts",
50
+ "rbt-esbuild.js",
51
+ "rbt.js",
52
+ "reboot.d.ts",
53
+ "reboot.js",
42
54
  "reboot_native.cc",
43
55
  "reboot_native.cjs",
44
56
  "reboot_native.d.ts",
45
- "reboot.js",
46
- "reboot.d.ts",
47
- "rbt.js",
48
- "rbt.d.ts",
49
- "venv.js",
57
+ "utils",
50
58
  "venv.d.ts",
59
+ "venv.js",
60
+ "version.d.ts",
51
61
  "version.js",
52
- "version.d.ts"
62
+ "workspaces.js"
53
63
  ],
54
64
  "exports": {
55
65
  "./package.json": "./package.json",
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "@bufbuild/protoc-gen-es/bin/protoc-gen-es";
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // NOTE: Yarn doesn't add transitive binary dependencies to PATH!!!
3
+ // Thus this script which "proxies" to `protoc-gen-es`.
4
+ import "@bufbuild/protoc-gen-es/bin/protoc-gen-es";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare const BUNDLE_PATH: string;
package/rbt-esbuild.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import esbuild from "esbuild";
4
+ import { writeFileSync } from "node:fs";
5
+ import * as path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { locateWorkspaces } from "./workspaces.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ // Expected usage: rbt-esbuild path/to/application.ts name
11
+ const args = process.argv.slice(2);
12
+ const application = args[0];
13
+ const name = args[1];
14
+ export const BUNDLE_PATH = path.join(__dirname, ".bundles", name);
15
+ let workspaces = [];
16
+ try {
17
+ workspaces = (await locateWorkspaces()).map(({ name }) => name);
18
+ }
19
+ catch (e) {
20
+ console.error(chalk.stderr.bold.red(`Failed to use your package manager to determine your workspaces (if any): ${e}`));
21
+ process.exit(-1);
22
+ }
23
+ const plugin = {
24
+ name: "rbt-esbuild",
25
+ setup(build) {
26
+ build.onResolve({ namespace: "file", filter: /.*/ }, (args) => {
27
+ // Do not mark as 'external' files starting with '.' or '/' because those
28
+ // are assumed to be local.
29
+ if (args.path.startsWith(".") || args.path.startsWith("/")) {
30
+ return null;
31
+ }
32
+ // Workspace modules are also local, not external.
33
+ for (const workspace of workspaces) {
34
+ if (args.path === workspace || args.path.startsWith(workspace + "/")) {
35
+ return null;
36
+ }
37
+ }
38
+ // Mark everything else external.
39
+ return { path: args.path, external: true };
40
+ });
41
+ },
42
+ };
43
+ esbuild
44
+ .build({
45
+ entryPoints: [application],
46
+ bundle: true,
47
+ platform: "node",
48
+ format: "esm",
49
+ metafile: true,
50
+ sourcemap: "inline",
51
+ banner: {
52
+ js: "/* eslint-disable */",
53
+ },
54
+ outfile: path.join(BUNDLE_PATH, "bundle.js"),
55
+ plugins: [plugin],
56
+ // This is only called by `rbt dev` and thus should only do module
57
+ // resolution for "development".
58
+ conditions: ["development"],
59
+ // TODO: support taking either `esbuild.config.js` or a more
60
+ // general `rbt.config.js` which would export a default object
61
+ // with options to pass on for specific esbuild, for example:
62
+ //
63
+ // export default {
64
+ // esbuild: {
65
+ // conditions: ["something"]
66
+ // }
67
+ // };
68
+ //
69
+ // We'll likely need to have users pass us the path to this file
70
+ // via some flag like
71
+ // `--esbuild-config=path/to/esbuild.config.js` or
72
+ // `--nodejs-config=path/to/rbt.config.js`.
73
+ //
74
+ // We could try to find the file but it's not obvious where to
75
+ // look. We could look for it based on `.rbtrc` or
76
+ // `--state-directory` (which ever was specified) but it's not
77
+ // obvious it should always be there, e.g., in the case of a
78
+ // monorepo with a `.rbtrc` that has "config" based flags.
79
+ //
80
+ // Once we have the file we can support overriding, for example:
81
+ //
82
+ // ...("conditions" in config.esbuild && { conditions: config.esbuild.conditions } || {}),
83
+ })
84
+ .then(async (result) => {
85
+ writeFileSync(path.join(BUNDLE_PATH, "meta.json"), JSON.stringify(result.metafile));
86
+ // Output the path where we bundled for `rbt dev` to consume.
87
+ console.log(BUNDLE_PATH);
88
+ process.exit(0);
89
+ })
90
+ .catch((error) => {
91
+ console.error(error instanceof Error ? error.message : error);
92
+ process.exit(-1);
93
+ });
package/rbt.js CHANGED
@@ -1,11 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "child_process";
3
3
  import * as path from "path";
4
+ import whichPMRuns from "which-pm-runs";
5
+ import { ensureYarnNodeLinker } from "./utils/index.js";
4
6
  import { ensurePythonVenv, VENV_EXEC_PATH } from "./venv.js";
7
+ async function ensureSupportedYarn() {
8
+ const packageManager = await whichPMRuns();
9
+ if (!packageManager) {
10
+ return;
11
+ }
12
+ if (packageManager.name === "yarn") {
13
+ ensureYarnNodeLinker();
14
+ }
15
+ }
16
+ function addExtensionlessToNodeOptions() {
17
+ // Add the `extensionless` loader to the `NODE_OPTIONS` env var so
18
+ // subsequent invocations of Node.js will use it. Depending on which
19
+ // version of Node.js we're using we have to add it differently.
20
+ const [major, minor] = process.versions.node.split(".").map(Number);
21
+ // Make sure that we can append to the `NODE_OPTIONS` env var.
22
+ process.env.NODE_OPTIONS =
23
+ (process.env.NODE_OPTIONS && process.env.NODE_OPTIONS + " ") || "";
24
+ // The `module.register()` function was added to Node.js in 20.6.0
25
+ // for the main release line and 18.19.0 for the LTS release line.
26
+ //
27
+ // If we have one of those two versions then we can pre import
28
+ // `extensionless/register` to use the `module.register()` function,
29
+ // otherwise we need to fall back to `--experimental-loader`.
30
+ if (major > 20 ||
31
+ (major == 20 && minor >= 6) ||
32
+ (major == 18 && minor >= 19)) {
33
+ process.env.NODE_OPTIONS += "--import=extensionless/register";
34
+ }
35
+ else {
36
+ process.env.NODE_OPTIONS += "--experimental-loader=extensionless";
37
+ }
38
+ }
5
39
  async function main() {
6
40
  ensurePythonVenv();
41
+ await ensureSupportedYarn();
7
42
  // Set env var to indicate that `rbt` is being invoked from Node.js.
8
43
  process.env.RBT_FROM_NODEJS = "true";
44
+ // Add extensionless loader.
45
+ addExtensionlessToNodeOptions();
9
46
  const rbt = spawnSync(`${path.join(VENV_EXEC_PATH, "rbt")} ${process.argv.slice(2).join(" ")}`, {
10
47
  stdio: [process.stdin, process.stdout, process.stderr],
11
48
  shell: true,
package/reboot.d.ts CHANGED
@@ -150,9 +150,10 @@ export declare class AllowAllIfAuthenticated extends Authorizer<protobuf_es.Mess
150
150
  }
151
151
  export declare class Application {
152
152
  #private;
153
- constructor({ servicers, initialize, tokenVerifier, }: {
153
+ constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }: {
154
154
  servicers: ServicerFactory[];
155
155
  initialize?: (context: ExternalContext) => Promise<void>;
156
+ initializeBearerToken?: string;
156
157
  tokenVerifier?: TokenVerifier;
157
158
  });
158
159
  run(): Promise<any>;
package/reboot.js CHANGED
@@ -21,15 +21,34 @@ export { reboot_native };
21
21
  export * from "./utils/index.js";
22
22
  const startedInstances = [];
23
23
  if (process != null) {
24
- const processExitHandler = () => {
25
- if (startedInstances.length) {
26
- console.warn(`${startedInstances.length} Reboot instance(s) still running on exit. Did you forget to run \`rbt.stop()\`?`);
24
+ const checkForStartedInstances = () => {
25
+ if (startedInstances.length > 0) {
26
+ console.warn(`${startedInstances.length} Reboot instance(s) still running on exit which may keep your process from exiting. Did you forget to run \`rbt.stop()\`?`);
27
27
  }
28
- process.exit(0);
29
28
  };
30
- process.on("exit", processExitHandler);
31
- process.on("SIGTERM", processExitHandler);
32
- process.on("SIGINT", processExitHandler);
29
+ process.on("exit", (code) => {
30
+ checkForStartedInstances();
31
+ });
32
+ const checkIfNoOtherListenersAndIfSoExit = (signal, code) => {
33
+ // Check if there is only one listener for this signal which would
34
+ // imply just us and if so then we need to perform the _default_
35
+ // act of calling `process.exit()`.
36
+ if (process.listeners(signal).length === 1) {
37
+ process.exit(code);
38
+ }
39
+ };
40
+ process.on("SIGTERM", () => {
41
+ checkForStartedInstances();
42
+ // NOTE: empirical testing shows that a Node.js process that
43
+ // exits due to a SIGTERM will exit with a code of 143.
44
+ checkIfNoOtherListenersAndIfSoExit("SIGTERM", 143);
45
+ });
46
+ process.on("SIGINT", () => {
47
+ checkForStartedInstances();
48
+ // NOTE: empirical testing shows that a Node.js process that
49
+ // exits due to a SIGINT will exit with a code of 130.
50
+ checkIfNoOtherListenersAndIfSoExit("SIGINT", 130);
51
+ });
33
52
  }
34
53
  export class Reboot {
35
54
  constructor() {
@@ -305,7 +324,7 @@ export class AllowAllIfAuthenticated extends Authorizer {
305
324
  }
306
325
  }
307
326
  export class Application {
308
- constructor({ servicers, initialize, tokenVerifier, }) {
327
+ constructor({ servicers, initialize, initializeBearerToken, tokenVerifier, }) {
309
328
  _Application_external.set(this, void 0);
310
329
  __classPrivateFieldSet(this, _Application_external, reboot_native.Application_constructor(ExternalContext.fromNativeExternal, servicers, async (context) => {
311
330
  if (initialize !== undefined) {
@@ -321,7 +340,7 @@ export class Application {
321
340
  }
322
341
  }
323
342
  }
324
- }, tokenVerifier), "f");
343
+ }, initializeBearerToken, tokenVerifier), "f");
325
344
  }
326
345
  async run() {
327
346
  return await reboot_native.Application_run(__classPrivateFieldGet(this, _Application_external, "f"));
package/reboot_native.cc CHANGED
@@ -1177,13 +1177,11 @@ py::object make_py_token_verifier(NapiSafeObjectReference js_token_verifier) {
1177
1177
  {Napi::Function::New(
1178
1178
  env,
1179
1179
  [py_future](const Napi::CallbackInfo& info) {
1180
- Napi::Object js_bytes_auth =
1181
- info[0].As<Napi::Object>();
1182
1180
  std::optional<std::string> bytes_auth;
1183
- if (!js_bytes_auth.IsNull()) {
1181
+ if (!info[0].IsNull()) {
1184
1182
  bytes_auth =
1185
1183
  uint8array_to_str(
1186
- js_bytes_auth.As<Napi::Uint8Array>());
1184
+ info[0].As<Napi::Uint8Array>());
1187
1185
  }
1188
1186
 
1189
1187
  adaptor->ScheduleCallbackOnPythonEventLoop(
@@ -1592,7 +1590,7 @@ Napi::Value Service_call(const Napi::CallbackInfo& info) {
1592
1590
 
1593
1591
  py::object py_task =
1594
1592
  py::module::import("reboot.nodejs.python")
1595
- .attr("create_task")(
1593
+ .attr("create_task_with_context")(
1596
1594
  py_service->attr(("_" + kind).c_str())(
1597
1595
  method,
1598
1596
  py_context,
@@ -1687,7 +1685,7 @@ Napi::Value Future_await(const Napi::CallbackInfo& info) {
1687
1685
  deferred = std::move(deferred)]() {
1688
1686
  py::object py_task =
1689
1687
  py::module::import("reboot.nodejs.python")
1690
- .attr("create_task")(
1688
+ .attr("create_task_with_context")(
1691
1689
  py_service->attr("_future_await")(
1692
1690
  method,
1693
1691
  py_context,
@@ -1837,9 +1835,14 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1837
1835
  auto js_initialize = NapiSafeFunctionReference(
1838
1836
  info[2].As<Napi::Function>());
1839
1837
 
1838
+ std::optional<std::string> initialize_bearer_token;
1839
+ if (!info[3].IsUndefined()) {
1840
+ initialize_bearer_token = info[3].As<Napi::String>().Utf8Value();
1841
+ }
1842
+
1840
1843
  std::optional<NapiSafeObjectReference> js_token_verifier;
1841
- if (info[3].IsObject()) {
1842
- js_token_verifier = NapiSafeReference(info[3].As<Napi::Object>());
1844
+ if (!info[4].IsUndefined()) {
1845
+ js_token_verifier = NapiSafeReference(info[4].As<Napi::Object>());
1843
1846
  }
1844
1847
 
1845
1848
  std::promise<py::object*> promise;
@@ -1847,6 +1850,7 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1847
1850
  adaptor->ScheduleCallbackOnPythonEventLoop(
1848
1851
  [&promise,
1849
1852
  servicer_details = std::move(servicer_details),
1853
+ initialize_bearer_token = std::move(initialize_bearer_token),
1850
1854
  js_initialize = std::move(js_initialize),
1851
1855
  js_token_verifier,
1852
1856
  js_from_native_external = std::move(js_from_native_external)]() {
@@ -1943,8 +1947,10 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1943
1947
  return py_future;
1944
1948
  });
1945
1949
 
1946
- py::object py_applications = py::module::import(
1947
- "reboot.aio.applications");
1950
+ py::object py_initialize_bearer_token = py::none();
1951
+ if (initialize_bearer_token.has_value()) {
1952
+ py_initialize_bearer_token = py::str(*initialize_bearer_token);
1953
+ }
1948
1954
 
1949
1955
  py::object py_token_verifier = py::none();
1950
1956
  if (js_token_verifier.has_value()) {
@@ -1952,10 +1958,14 @@ Napi::Value Application_constructor(const Napi::CallbackInfo& info) {
1952
1958
  }
1953
1959
 
1954
1960
  promise.set_value(
1955
- new py::object(py_applications.attr("Application")(
1956
- "servicers"_a = py_servicers,
1957
- "initialize"_a = py_initialize,
1958
- "token_verifier"_a = py_token_verifier)));
1961
+ new py::object(
1962
+ py::module::import("reboot.aio.applications")
1963
+ .attr("Application")(
1964
+ "servicers"_a = py_servicers,
1965
+ "initialize"_a = py_initialize,
1966
+ "initialize_bearer_token"_a =
1967
+ py_initialize_bearer_token,
1968
+ "token_verifier"_a = py_token_verifier)));
1959
1969
  }
1960
1970
  // TODO(benh): improve error handling mechanism to force all
1961
1971
  // raised exceptions to be handled.
@@ -2016,9 +2026,10 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
2016
2026
  py_application,
2017
2027
  deferred = std::move(deferred)]() {
2018
2028
  py::object py_task =
2019
- py::module::import("asyncio").attr("create_task")(
2020
- py_application->attr("run")(),
2021
- "name"_a = "Application.run() in nodejs");
2029
+ py::module::import("reboot.nodejs.python")
2030
+ .attr("create_task")(
2031
+ py_application->attr("run")(),
2032
+ "name"_a = "Application.run() in nodejs");
2022
2033
 
2023
2034
  py_task.attr("add_done_callback")(py::cpp_function(
2024
2035
  [deferred = std::move(deferred),
@@ -2041,9 +2052,16 @@ Napi::Value Application_run(const Napi::CallbackInfo& info) {
2041
2052
  if (!exception.has_value()) {
2042
2053
  deferred->Resolve(env.Null());
2043
2054
  } else {
2055
+ env.Global()
2056
+ .Get("process")
2057
+ .As<Napi::Object>()
2058
+ .Set("exitCode", 1);
2044
2059
  deferred->Reject(
2045
2060
  Napi::Error::New(env, *exception).Value());
2046
2061
  }
2062
+ // When `Application.run` exits, we unref our
2063
+ // thread_safe_function to cause the runtime to exit.
2064
+ adaptor->thread_safe_function.Unref(env);
2047
2065
  });
2048
2066
 
2049
2067
  delete py_task;
@@ -2340,7 +2358,7 @@ Napi::Value retry_reactively_until(const Napi::CallbackInfo& info) {
2340
2358
 
2341
2359
  py::object py_task =
2342
2360
  py::module::import("reboot.nodejs.python")
2343
- .attr("create_task")(
2361
+ .attr("create_task_with_context")(
2344
2362
  (py::module::import("reboot.aio.contexts")
2345
2363
  .attr("retry_reactively_until"))(
2346
2364
  py_context,
@@ -2476,7 +2494,7 @@ Napi::Value atLeastOrMostOnce(const Napi::CallbackInfo& info) {
2476
2494
 
2477
2495
  py::object py_task =
2478
2496
  py::module::import("reboot.nodejs.python")
2479
- .attr("create_task")(
2497
+ .attr("create_task_with_context")(
2480
2498
  (py::module::import("reboot.aio.memoize").attr("memoize"))(
2481
2499
  py::str(idempotency_alias),
2482
2500
  py_context,
@@ -2553,7 +2571,7 @@ Napi::Value Servicer_read(const Napi::CallbackInfo& info) {
2553
2571
  deferred = std::move(deferred)]() {
2554
2572
  py::object py_task =
2555
2573
  py::module::import("reboot.nodejs.python")
2556
- .attr("create_task")(
2574
+ .attr("create_task_with_context")(
2557
2575
  py_servicer->attr("_read")(py_context),
2558
2576
  py_context,
2559
2577
  "name"_a = "servicer._read(...) in nodejs");
@@ -2702,7 +2720,7 @@ Napi::Value Servicer_write(const Napi::CallbackInfo& info) {
2702
2720
 
2703
2721
  py::object py_task =
2704
2722
  py::module::import("reboot.nodejs.python")
2705
- .attr("create_task")(
2723
+ .attr("create_task_with_context")(
2706
2724
  py_servicer->attr("_write")(
2707
2725
  py_context,
2708
2726
  py_writer,
package/reboot_native.cjs CHANGED
@@ -7,7 +7,7 @@ const [major, minor, patch] = process.versions.node.split(".").map(Number);
7
7
 
8
8
  if (major < 18) {
9
9
  console.error(
10
- chalk.red(
10
+ chalk.stderr.bold.red(
11
11
  `Reboot requires nodejs version >=18 (found ${major}.${minor}.${patch})`
12
12
  )
13
13
  );
@@ -16,21 +16,16 @@ if (major < 18) {
16
16
 
17
17
  const reboot_native = { exports: {} };
18
18
 
19
- // Next.js has its own runtime environment that runs on top of Node. Thus,
20
- // __dirname isn't correctly defined in a Next.js app.
21
- // We special case for Next.js for this reason.
22
- // https://stackoverflow.com/questions/77589486/nextjs-cannot-find-dirname
23
- // TODO: find a more permanent solution for this.
24
- let node_modules_reboot_directory;
19
+ // If we are in a Next.js context, check that serverExternalPackages is
20
+ // correctly set. If not correctly set, Reboot backend TS code will not work
21
+ // with React Server Components inside Next.
22
+
25
23
  if (__dirname.includes(".next")) {
26
- node_modules_reboot_directory = path.join(
27
- process.cwd(),
28
- "node_modules",
29
- "@reboot-dev",
30
- "reboot"
24
+ throw new Error(
25
+ "For Next.js to work correctly with Reboot native code, you " +
26
+ "must include option 'serverExternalPackages: ['@reboot-dev/reboot']' " +
27
+ "in your next.config.ts"
31
28
  );
32
- } else {
33
- node_modules_reboot_directory = __dirname;
34
29
  }
35
30
 
36
31
  // NOTE: as of Python 3.8 we _must_ load the Python library via
@@ -41,12 +36,7 @@ process.dlopen(
41
36
  reboot_native,
42
37
  // TODO(benh): consider using `require(bindings)` to properly locate
43
38
  // 'reboot_native.node'.
44
- path.join(
45
- node_modules_reboot_directory,
46
- "build",
47
- "Release",
48
- "reboot_native.node"
49
- ),
39
+ path.join(__dirname, "build", "Release", "reboot_native.node"),
50
40
  os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL
51
41
  );
52
42
 
package/utils/index.d.ts CHANGED
@@ -1 +1,12 @@
1
1
  export * from "./errors.js";
2
+ export declare function parseVersion(version: string): [number, number, number];
3
+ export type Version = [number, number, number];
4
+ export declare function supportedVersion({ required: [requiredMajor, requiredMinor, requiredPatch], found: [foundMajor, foundMinor, foundPatch], }: {
5
+ required: Version;
6
+ found: Version;
7
+ }): boolean;
8
+ export declare function ensureYarnNodeLinker(): void;
9
+ /**
10
+ * Spawn a process with the given arguments, and return its stdout as a string.
11
+ */
12
+ export declare function spawnAndReturnStdout(command: string, args: string[]): string;
package/utils/index.js CHANGED
@@ -1 +1,47 @@
1
+ import chalk from "chalk";
2
+ import { spawnSync } from "child_process";
1
3
  export * from "./errors.js";
4
+ export function parseVersion(version) {
5
+ const match = version.trim().match(/(\d+\.\d+\.\d+)/);
6
+ if (!match) {
7
+ throw new Error(`Failed to parse '${version}' into a version`);
8
+ }
9
+ return version.split(".").map(Number);
10
+ }
11
+ export function supportedVersion({ required: [requiredMajor, requiredMinor, requiredPatch], found: [foundMajor, foundMinor, foundPatch], }) {
12
+ return (foundMajor > requiredMajor ||
13
+ (foundMajor === requiredMajor && foundMinor > requiredMinor) ||
14
+ (foundMajor === requiredMajor &&
15
+ foundMinor === requiredMinor &&
16
+ foundPatch >= requiredPatch));
17
+ }
18
+ export function ensureYarnNodeLinker() {
19
+ // Ensure that they aren't using Yarn Plug'n'Play which we don't yet
20
+ // support.
21
+ const nodeLinker = spawnAndReturnStdout("yarn", [
22
+ "config",
23
+ "get",
24
+ "nodeLinker",
25
+ ]);
26
+ if (nodeLinker.trim() !== "node-modules") {
27
+ console.error(chalk.stderr.bold.red("Yarn Plug'n'Play is not yet supported, you must use 'node-modules' as your 'nodeLinker'"));
28
+ process.exit(-1);
29
+ }
30
+ }
31
+ /**
32
+ * Spawn a process with the given arguments, and return its stdout as a string.
33
+ */
34
+ export function spawnAndReturnStdout(command, args) {
35
+ const result = spawnSync(command, args, {
36
+ stdio: ["ignore", "pipe", "inherit"],
37
+ });
38
+ if (result.error) {
39
+ throw new Error(`Failed to run '${command} ${args.join(" ")}': ${result.error}\n` +
40
+ `\n` +
41
+ `Please report this bug to the maintainers!`);
42
+ }
43
+ else if (result.status !== 0) {
44
+ throw new Error(`Running '${command} ${args.join(" ")}' exited with status ${result.status}. Please report this bug including the output of that command (if any) to the maintainers!`);
45
+ }
46
+ return result.stdout.toString();
47
+ }
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const REBOOT_VERSION = "0.19.0";
1
+ export declare const REBOOT_VERSION = "0.19.2";
package/version.js CHANGED
@@ -1 +1 @@
1
- export const REBOOT_VERSION = "0.19.0";
1
+ export const REBOOT_VERSION = "0.19.2";
package/workspaces.js ADDED
@@ -0,0 +1,120 @@
1
+ import whichPMRuns from "which-pm-runs";
2
+ import { parseVersion, spawnAndReturnStdout, supportedVersion, } from "./utils/index.js";
3
+ /**
4
+ * Returns package-manager Workspaces of which a project in the given directory
5
+ * is a member.
6
+ */
7
+ export async function locateWorkspaces() {
8
+ const packageManager = await whichPMRuns();
9
+ if (!packageManager) {
10
+ return null;
11
+ }
12
+ // NOTE: These methods will error if they fail to extract
13
+ // workspaces, based on the assumption that `whichPMRuns` will do a
14
+ // reasonable job of detecting a package manager, otherwise throws
15
+ // an exception.
16
+ switch (packageManager.name) {
17
+ case "yarn": {
18
+ return extractYarnWorkspaces();
19
+ }
20
+ case "npm": {
21
+ return extractNpmWorkspaces();
22
+ }
23
+ case "pnpm": {
24
+ return extractPnpmWorkspaces();
25
+ }
26
+ default: {
27
+ throw new Error(`Unsupported package manager '${packageManager}'`);
28
+ }
29
+ }
30
+ }
31
+ function extractYarnWorkspaces() {
32
+ // TODO: what is the actual minimum version we need?
33
+ const YARN_VERSION_REQUIRED = "4.0.0";
34
+ const required = parseVersion(YARN_VERSION_REQUIRED);
35
+ const found = parseVersion(spawnAndReturnStdout("yarn", ["--version"]));
36
+ if (!supportedVersion({ required, found })) {
37
+ throw new Error(`yarn version >=${YARN_VERSION_REQUIRED} is required, found ${found.join(".")}`);
38
+ }
39
+ const command = "yarn";
40
+ const args = ["workspaces", "list", "--json", "--recursive"];
41
+ // `yarn workspaces list --json` returns JSON lines, not complete
42
+ // JSON, so we have to do some extra parsing ourselves.
43
+ return spawnAndReturnStdout(command, args)
44
+ .trim()
45
+ .split("\n")
46
+ .map((line) => {
47
+ const { name } = JSON.parse(line.trim());
48
+ return { name };
49
+ });
50
+ }
51
+ function extractNpmWorkspaces() {
52
+ // We need >=8.16.0 which is when `npm query` was introduced.
53
+ const NPM_VERSION_REQUIRED = "8.16.0";
54
+ const required = parseVersion(NPM_VERSION_REQUIRED);
55
+ const found = parseVersion(spawnAndReturnStdout("npm", ["--version"]));
56
+ if (!supportedVersion({ required, found })) {
57
+ throw new Error(`npm version >=${NPM_VERSION_REQUIRED} is required, found ${found.join(".")}`);
58
+ }
59
+ const command = "npm";
60
+ const args = ["query", ".workspace"];
61
+ const workspaces = spawnAndParseJSON(command, args).map(({ name }) => ({
62
+ name,
63
+ }));
64
+ // Include the package name as it might be used and it is consistent
65
+ // with both `yarn` and `pnpm` which always include the top-level
66
+ // package name even if they aren't explicitly in `workspaces`.
67
+ const name = spawnAndParseJSON("npm", ["pkg", "get", "name"]);
68
+ // `npm pkg get name` returns an empty object if there is no
69
+ // top-level package name.
70
+ if (Object.keys(name).length === 0) {
71
+ return workspaces;
72
+ }
73
+ else if (typeof name !== "string") {
74
+ throw new Error(`Failed to get name of your package. Please report this issue to the maintainers!`);
75
+ }
76
+ return [{ name }, ...workspaces];
77
+ }
78
+ function extractPnpmWorkspaces() {
79
+ // TODO: is there a minimum version required?
80
+ const command = "pnpm";
81
+ const args = ["list", "--recursive", "--depth", "-1", "--only-projects"];
82
+ // `pnpm list --recursive --depth -1 --only-projects --json` emits something
83
+ // which is not quite valid JSON: closer to JSON lines, but not easily splittable.
84
+ //
85
+ // Instead, we parse the human-readable output -- a series of lines like:
86
+ // @reboot-pm/prosemirror@0.0.1 /Users/example/src/repo_root/project_dir (PRIVATE)
87
+ return spawnAndReturnStdout(command, args)
88
+ .trim()
89
+ .split("\n")
90
+ .map((line) => {
91
+ const parts = line.trim().split(" ");
92
+ if (parts.length !== 3) {
93
+ throw new Error(`Unexpected output from '${command} ${args.join(" ")}'. Please report the output of that command to the maintainers!`);
94
+ }
95
+ // Strip off the version from the name.
96
+ const lastAtSign = parts[0].lastIndexOf("@");
97
+ let name;
98
+ if (lastAtSign !== -1 && lastAtSign !== 0) {
99
+ name = parts[0].substring(0, lastAtSign);
100
+ }
101
+ else {
102
+ name = parts[0];
103
+ }
104
+ return { name };
105
+ });
106
+ }
107
+ /**
108
+ * Spawn a process with the given arguments, and parse its stdout as JSON.
109
+ *
110
+ * Raises an error if the process cannot be spawned, or if JSON cannot be parsed.
111
+ */
112
+ function spawnAndParseJSON(command, args) {
113
+ const stdout = spawnAndReturnStdout(command, args);
114
+ try {
115
+ return JSON.parse(stdout);
116
+ }
117
+ catch (error) {
118
+ throw new Error(`Failed to parse output of '${command} ${args.join(" ")}' as JSON: ${error}`);
119
+ }
120
+ }