esm.dev 2.0.3 → 2.0.5

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 (64) hide show
  1. package/dist/cli.js +20 -0
  2. package/dist/commands/ESMDevCommand.js +6 -0
  3. package/dist/commands/InitCommand.js +49 -0
  4. package/dist/commands/LoginCommand.js +12 -0
  5. package/dist/commands/MustacheGeneratorCommand.js +9 -0
  6. package/dist/commands/RepublishCommand.js +11 -0
  7. package/dist/commands/StartCommand.js +18 -0
  8. package/dist/commands/TokenCommand.js +23 -0
  9. package/dist/commands/VersionCommand.js +15 -0
  10. package/dist/commands/WaitForRegistryCommand.js +12 -0
  11. package/dist/commands/WatchCommand.js +12 -0
  12. package/dist/commands/mixins/CommandClass.js +1 -0
  13. package/dist/commands/mixins/ESMServed.js +9 -0
  14. package/dist/commands/mixins/ESMStorageSpecific.js +9 -0
  15. package/dist/commands/mixins/PackagePathSpecific.js +14 -0
  16. package/dist/commands/mixins/RegistrySpecific.js +9 -0
  17. package/dist/commands/mixins/Retryable.js +29 -0
  18. package/dist/commands/mixins/Servable.js +11 -0
  19. package/dist/commands/mixins/Watchable.js +9 -0
  20. package/dist/commands/options/EnvOption.js +6 -0
  21. package/dist/lib/MustacheGenerator.js +9 -0
  22. package/dist/lib/getPackageMeta.js +10 -0
  23. package/dist/lib/index.js +8 -0
  24. package/dist/lib/login.js +25 -0
  25. package/dist/lib/publish.js +7 -0
  26. package/dist/lib/queue.js +48 -0
  27. package/dist/lib/republish.js +9 -0
  28. package/dist/lib/server.js +34 -0
  29. package/dist/lib/unpublish.js +26 -0
  30. package/dist/lib/until.js +32 -0
  31. package/dist/lib/watch.js +88 -0
  32. package/dist/lib/watchIgnoreList.js +50 -0
  33. package/package.json +5 -7
  34. package/src/cli.ts +0 -21
  35. package/src/commands/ESMDevCommand.ts +0 -8
  36. package/src/commands/InitCommand.ts +0 -71
  37. package/src/commands/LoginCommand.ts +0 -15
  38. package/src/commands/MustacheGeneratorCommand.ts +0 -10
  39. package/src/commands/RepublishCommand.ts +0 -15
  40. package/src/commands/StartCommand.ts +0 -23
  41. package/src/commands/TokenCommand.ts +0 -29
  42. package/src/commands/VersionCommand.ts +0 -23
  43. package/src/commands/WaitForRegistryCommand.ts +0 -17
  44. package/src/commands/WatchCommand.ts +0 -15
  45. package/src/commands/mixins/CommandClass.ts +0 -3
  46. package/src/commands/mixins/ESMServed.ts +0 -12
  47. package/src/commands/mixins/ESMStorageSpecific.ts +0 -16
  48. package/src/commands/mixins/PackagePathSpecific.ts +0 -18
  49. package/src/commands/mixins/RegistrySpecific.ts +0 -12
  50. package/src/commands/mixins/Retryable.ts +0 -33
  51. package/src/commands/mixins/Servable.ts +0 -14
  52. package/src/commands/mixins/Watchable.ts +0 -13
  53. package/src/commands/options/EnvOption.ts +0 -27
  54. package/src/lib/MustacheGenerator.ts +0 -10
  55. package/src/lib/getPackageMeta.ts +0 -15
  56. package/src/lib/login.ts +0 -29
  57. package/src/lib/publish.ts +0 -14
  58. package/src/lib/queue.ts +0 -59
  59. package/src/lib/republish.ts +0 -16
  60. package/src/lib/server.ts +0 -33
  61. package/src/lib/unpublish.ts +0 -40
  62. package/src/lib/until.ts +0 -50
  63. package/src/lib/watch.ts +0 -134
  64. package/src/lib/watchIgnoreList.ts +0 -61
package/dist/cli.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { runExit } from 'clipanion';
3
+ import { WatchCommand } from './commands/WatchCommand.js';
4
+ import { RepublishCommand } from './commands/RepublishCommand.js';
5
+ import { LoginCommand } from './commands/LoginCommand.js';
6
+ import { TokenCommand } from './commands/TokenCommand.js';
7
+ import { WaitForRegistryCommand } from './commands/WaitForRegistryCommand.js';
8
+ import { StartCommand } from './commands/StartCommand.js';
9
+ import { InitCommand } from './commands/InitCommand.js';
10
+ import { VersionCommand } from './commands/VersionCommand.js';
11
+ runExit([
12
+ InitCommand,
13
+ LoginCommand,
14
+ TokenCommand,
15
+ RepublishCommand,
16
+ StartCommand,
17
+ VersionCommand,
18
+ WaitForRegistryCommand,
19
+ WatchCommand,
20
+ ]);
@@ -0,0 +1,6 @@
1
+ import { Command } from 'clipanion';
2
+ import { RegistrySpecific } from './mixins/RegistrySpecific.js';
3
+ import { ESMStorageSpecific } from './mixins/ESMStorageSpecific.js';
4
+ import { PackagePathSpecific } from './mixins/PackagePathSpecific.js';
5
+ export class ESMDevCommand extends ESMStorageSpecific(RegistrySpecific(PackagePathSpecific(Command))) {
6
+ }
@@ -0,0 +1,49 @@
1
+ import { PackagePathSpecific } from './mixins/PackagePathSpecific.js';
2
+ import { MustacheGeneratorCommand } from './MustacheGeneratorCommand.js';
3
+ import { Option } from 'clipanion';
4
+ import { isNumber } from 'typanion';
5
+ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
6
+ static paths = [['init']];
7
+ static usage = this.Usage({
8
+ description: 'Initialises your repo ready to work with ESM.sh locally',
9
+ examples: [
10
+ ['Create a minimal template', 'esm.dev init'],
11
+ ["Specify the packages you're developing", 'esm.dev init packages/*'],
12
+ [
13
+ 'Specify different ports',
14
+ 'esm.dev init --port 3001 --esm-port 8081 --registry-port 4444',
15
+ ],
16
+ ],
17
+ });
18
+ esmPort = Option.String('-e,--esm-port', '8080', {
19
+ description: 'The port of the ESM Server',
20
+ validator: isNumber(),
21
+ });
22
+ esmStoragePath = Option.String('-s,--esm-storage-path', './docker-storage/esm', {
23
+ description: "Path to ESM.sh's storage",
24
+ });
25
+ port = Option.String('-p,--port', '3000', {
26
+ description: "The server's port",
27
+ });
28
+ outputDirectory = Option.String('-o,--output-dir', '.', {
29
+ description: 'The output directory',
30
+ });
31
+ registryPort = Option.String('-r,--registry-port', '4873', {
32
+ description: 'The port of your local registry',
33
+ validator: isNumber(),
34
+ });
35
+ esmURL;
36
+ npmRegistryURL;
37
+ packages;
38
+ async execute() {
39
+ const path = await import('node:path');
40
+ this.templateDir = path.resolve(import.meta.dirname, '..', '..', 'templates', 'init');
41
+ this.destinationDir = this.outputDirectory;
42
+ this.packages = this.packagePaths.map((packagePath) => ({
43
+ path: packagePath,
44
+ basename: path.basename(packagePath),
45
+ }));
46
+ await this.removeDestinationFile('docker-compose.yaml', 'docker-compose.yml.mustache');
47
+ await super.execute();
48
+ }
49
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from 'clipanion';
2
+ import { RegistrySpecific } from './mixins/RegistrySpecific.js';
3
+ export class LoginCommand extends RegistrySpecific(Command) {
4
+ static paths = [['login']];
5
+ static usage = this.Usage({
6
+ description: 'Login in to the NPM registry',
7
+ });
8
+ async execute() {
9
+ const { login } = await import('../lib/login.js');
10
+ return login(this.registry);
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { GeneratorCommand } from 'clipanion-generator-command';
2
+ import { MustacheGenerator } from '../lib/MustacheGenerator.js';
3
+ export class MustacheGeneratorCommand extends GeneratorCommand {
4
+ get generator() {
5
+ return new MustacheGenerator(this.templateDir, this.destinationDir, [
6
+ '.mustache',
7
+ ]);
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ import { ESMDevCommand } from './ESMDevCommand.js';
2
+ export class RepublishCommand extends ESMDevCommand {
3
+ static paths = [['republish']];
4
+ static usage = this.Usage({
5
+ description: 'Removes the references, of given packages, from the ESM server and registry and republishes.',
6
+ });
7
+ async execute() {
8
+ const { republish } = await import('../lib/republish.js');
9
+ await this.eachPackagePath((packagePath) => republish(packagePath, this));
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ import { ESMDevCommand } from './ESMDevCommand.js';
2
+ import { Servable } from './mixins/Servable.js';
3
+ import { Watchable } from './mixins/Watchable.js';
4
+ import { ESMServed } from './mixins/ESMServed.js';
5
+ export class StartCommand extends Servable(Watchable(ESMServed(ESMDevCommand))) {
6
+ static paths = [['start']];
7
+ static usage = this.Usage({
8
+ description: 'Runs a server and concurrently watches a file system',
9
+ });
10
+ async execute() {
11
+ const [{ watch }, { serve }] = await Promise.all([
12
+ import('../lib/watch.js'),
13
+ import('../lib/server.js'),
14
+ ]);
15
+ serve(this.port, this.esmOrigin);
16
+ await this.eachPackagePath((packagePath) => watch(packagePath, this));
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from 'clipanion';
2
+ import { RegistrySpecific } from './mixins/RegistrySpecific.js';
3
+ export class TokenCommand extends RegistrySpecific(Command) {
4
+ static paths = [['token']];
5
+ static usage = this.Usage({
6
+ description: 'Get the NPM token',
7
+ });
8
+ async execute() {
9
+ const [{ default: escapeStringRegexp }, { readFile }] = await Promise.all([
10
+ import('escape-string-regexp'),
11
+ import('node:fs/promises'),
12
+ ]);
13
+ const npmrc = await readFile('~/.npmrc', 'utf-8');
14
+ const url = new URL(this.registry);
15
+ const regex = new RegExp(`^//${escapeStringRegexp(url.hostname)}/:_authToken=(.*)$`, 'm');
16
+ const match = regex.exec(npmrc);
17
+ if (!match) {
18
+ this.context.stderr.write(`Token has not been created yet\n`);
19
+ return 1;
20
+ }
21
+ this.context.stdout.write(match[1] + '\n');
22
+ }
23
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from 'clipanion';
2
+ export class VersionCommand extends Command {
3
+ static paths = [['version']];
4
+ static usage = this.Usage({
5
+ description: 'Display the version of esm.dev',
6
+ });
7
+ async execute() {
8
+ const [path, { readFile }] = await Promise.all([
9
+ import('node:path'),
10
+ import('node:fs/promises'),
11
+ ]);
12
+ const pckg = JSON.parse(await readFile(path.resolve(import.meta.dirname, '..', '..', 'package.json'), 'utf-8'));
13
+ this.context.stdout.write(`${pckg.version}\n`);
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from 'clipanion';
2
+ import { RegistrySpecific } from './mixins/RegistrySpecific.js';
3
+ import { Retryable } from './mixins/Retryable.js';
4
+ export class WaitForRegistryCommand extends Retryable(RegistrySpecific(Command)) {
5
+ static paths = [['wait-for-registry'], ['wfr']];
6
+ static usage = this.Usage({
7
+ description: 'wait for the registry to be online',
8
+ });
9
+ async execute() {
10
+ return this.retry(this.registry);
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import { ESMDevCommand } from './ESMDevCommand.js';
2
+ import { Watchable } from './mixins/Watchable.js';
3
+ export class WatchCommand extends Watchable(ESMDevCommand) {
4
+ static paths = [['watch']];
5
+ static usage = this.Usage({
6
+ description: 'Watches directories and republishes on changes',
7
+ });
8
+ async execute() {
9
+ const { watch } = await import('../lib/watch.js');
10
+ await this.eachPackagePath((packagePath) => watch(packagePath, this));
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { EnvOption } from '../options/EnvOption.js';
2
+ export function ESMServed(Base) {
3
+ class ESMServed extends Base {
4
+ esmOrigin = EnvOption('ESM_ORIGIN', '-e,--esm-origin', {
5
+ description: 'The base URL of the ESM Server',
6
+ });
7
+ }
8
+ return ESMServed;
9
+ }
@@ -0,0 +1,9 @@
1
+ import { EnvOption } from '../options/EnvOption.js';
2
+ export function ESMStorageSpecific(Base) {
3
+ class ESMStorageSpecific extends Base {
4
+ esmStoragePath = EnvOption('ESM_STORAGE_PATH', '-s,--esm-storage-path', {
5
+ description: "Path to ESM.sh's storage",
6
+ });
7
+ }
8
+ return ESMStorageSpecific;
9
+ }
@@ -0,0 +1,14 @@
1
+ import { Option } from 'clipanion';
2
+ export function PackagePathSpecific(Base) {
3
+ class PackagePathSpecific extends Base {
4
+ packagePaths = Option.Rest({
5
+ name: 'packages',
6
+ });
7
+ async eachPackagePath(cb) {
8
+ const { glob } = await import('glob');
9
+ const packagePaths = await glob(this.packagePaths);
10
+ await Promise.all(packagePaths.map(cb));
11
+ }
12
+ }
13
+ return PackagePathSpecific;
14
+ }
@@ -0,0 +1,9 @@
1
+ import { EnvOption } from '../options/EnvOption.js';
2
+ export function RegistrySpecific(Base) {
3
+ class RegistrySpecific extends Base {
4
+ registry = EnvOption('NPM_REGISTRY', '-r,--registry', {
5
+ description: 'The URL of your local registry',
6
+ });
7
+ }
8
+ return RegistrySpecific;
9
+ }
@@ -0,0 +1,29 @@
1
+ import { Option } from 'clipanion';
2
+ import { isNumber } from 'typanion';
3
+ export function Retryable(Base) {
4
+ class Retryable extends Base {
5
+ timeout = Option.String('-t,--timeout', '10000', {
6
+ description: 'The amount of total ms to keep trying for',
7
+ validator: isNumber(),
8
+ });
9
+ interval = Option.String('-i,--interval', '300', {
10
+ description: 'The amount of ms inbetween each attempt',
11
+ validator: isNumber(),
12
+ });
13
+ async retry(endpoint) {
14
+ const { EndpointUnavailableError, waitForEndpoint } = await import('../../lib/until.js');
15
+ try {
16
+ await waitForEndpoint({ ...this, endpoint });
17
+ }
18
+ catch (error) {
19
+ if (error instanceof EndpointUnavailableError) {
20
+ this.context.stderr.write(`${endpoint} is not available\n`);
21
+ return 1;
22
+ }
23
+ else
24
+ throw error;
25
+ }
26
+ }
27
+ }
28
+ return Retryable;
29
+ }
@@ -0,0 +1,11 @@
1
+ import { isNumber } from 'typanion';
2
+ import { EnvOption } from '../options/EnvOption.js';
3
+ export function Servable(Base) {
4
+ class Servable extends Base {
5
+ port = EnvOption('PORT', '-p,--port', {
6
+ description: 'The port to run the server on',
7
+ validator: isNumber(),
8
+ });
9
+ }
10
+ return Servable;
11
+ }
@@ -0,0 +1,9 @@
1
+ import { Option } from 'clipanion';
2
+ export function Watchable(Base) {
3
+ class Watchable extends Base {
4
+ legacyMethod = Option.Boolean('-l,--legacy-method', false, {
5
+ description: 'Use a less performant, legacy, watch method. Sometimes needed in docker or vagrant environments.',
6
+ });
7
+ }
8
+ return Watchable;
9
+ }
@@ -0,0 +1,6 @@
1
+ import { Option } from 'clipanion';
2
+ export function EnvOption(envName, descriptor, opts = {}) {
3
+ return process.env[envName]
4
+ ? Option.String(descriptor, process.env[envName], opts)
5
+ : Option.String(descriptor, { ...opts, required: true });
6
+ }
@@ -0,0 +1,9 @@
1
+ import Mustache from 'mustache';
2
+ import { Generator } from 'clipanion-generator-command/Generator';
3
+ export class MustacheGenerator extends Generator {
4
+ renderTemplate(templateContext, template) {
5
+ return Mustache.render(template, templateContext, undefined, {
6
+ escape: (x) => x,
7
+ });
8
+ }
9
+ }
@@ -0,0 +1,10 @@
1
+ import { readFile, stat as getStat } from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ export async function getPackageMeta(packagePath) {
4
+ const stat = await getStat(packagePath);
5
+ const packageRoot = stat.isDirectory()
6
+ ? packagePath
7
+ : path.dirname(packagePath);
8
+ const pckg = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf-8'));
9
+ return { name: pckg.name, packageRoot, private: pckg.private };
10
+ }
@@ -0,0 +1,8 @@
1
+ export * from './login.js';
2
+ export * from './publish.js';
3
+ export * from './queue.js';
4
+ export * from './republish.js';
5
+ export * from './unpublish.js';
6
+ export * from './until.js';
7
+ export * from './watch.js';
8
+ export * from './watchIgnoreList.js';
@@ -0,0 +1,25 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function login(registry) {
3
+ return new Promise((resolve) => {
4
+ const child = spawn('npm', ['login', '--registry', registry, '--quiet']);
5
+ child.stderr.on('data', (d) => console.error(d.toString()));
6
+ child.stdout.on('data', (d) => {
7
+ const data = d.toString();
8
+ switch (true) {
9
+ case /username/i.test(data):
10
+ child.stdin.write('esm.dev\n');
11
+ break;
12
+ case /password/i.test(data):
13
+ child.stdin.write('esm.dev\n');
14
+ break;
15
+ case /email/i.test(data):
16
+ child.stdin.write('esm.dev@esm.dev.com');
17
+ break;
18
+ case /logged in as/i.test(data):
19
+ child.stdin.end();
20
+ break;
21
+ }
22
+ });
23
+ child.on('exit', resolve);
24
+ });
25
+ }
@@ -0,0 +1,7 @@
1
+ import { $ } from 'zx';
2
+ import * as path from 'node:path';
3
+ export async function publish({ packageRoot, registry, }) {
4
+ await $({
5
+ cwd: path.resolve(packageRoot),
6
+ }) `npm publish --registry ${registry}`;
7
+ }
@@ -0,0 +1,48 @@
1
+ import { debounce, Mutex } from 'es-toolkit';
2
+ const mutex = new Mutex();
3
+ export async function queue(fn, signal) {
4
+ await mutex.acquire();
5
+ try {
6
+ signal?.throwIfAborted();
7
+ return await fn();
8
+ }
9
+ finally {
10
+ mutex.release();
11
+ }
12
+ }
13
+ /**
14
+ * Just like debounce, but the first call will add a promise to the queue
15
+ * and that promise won't resolve until the debounced function is finally called.
16
+ */
17
+ export function queuedDebounce(fn, delay, signal) {
18
+ let promiseWithResolvers;
19
+ let queuedPromise;
20
+ signal?.addEventListener('abort', (reason) => {
21
+ debounced.cancel();
22
+ promiseWithResolvers?.reject(reason);
23
+ });
24
+ const debounced = debounce(async (...args) => {
25
+ try {
26
+ const result = await fn(...args);
27
+ promiseWithResolvers?.resolve(result);
28
+ }
29
+ catch (error) {
30
+ promiseWithResolvers?.reject(error);
31
+ }
32
+ finally {
33
+ promiseWithResolvers = undefined;
34
+ }
35
+ }, delay, { signal });
36
+ return (...args) => {
37
+ const promise = start();
38
+ debounced(...args);
39
+ return promise;
40
+ };
41
+ function start() {
42
+ if (!queuedPromise) {
43
+ promiseWithResolvers = Promise.withResolvers();
44
+ queuedPromise = queue(async () => promiseWithResolvers?.promise);
45
+ }
46
+ return queuedPromise;
47
+ }
48
+ }
@@ -0,0 +1,9 @@
1
+ import { getPackageMeta } from './getPackageMeta.js';
2
+ import { publish } from './publish.js';
3
+ import { unpublish } from './unpublish.js';
4
+ export async function republish(packagePath, opts) {
5
+ console.info(`Republishing ${packagePath}`);
6
+ const { name, packageRoot } = await getPackageMeta(packagePath);
7
+ await unpublish({ ...opts, name });
8
+ await publish({ ...opts, packageRoot });
9
+ }
@@ -0,0 +1,34 @@
1
+ import { queue } from './queue.js';
2
+ import { createServer } from 'node:http';
3
+ import httpProxy from 'http-proxy';
4
+ export async function serve(port, esmOrigin) {
5
+ const proxy = httpProxy.createProxyServer();
6
+ const { promise, resolve } = Promise.withResolvers();
7
+ const server = createServer((req, res) => {
8
+ queue(() => new Promise((resolve, reject) => {
9
+ console.info('Proxying', req.url);
10
+ req.on('error', reject);
11
+ res.on('error', reject);
12
+ res.on('close', resolve);
13
+ proxy.web(req, res, { followRedirects: true, target: esmOrigin });
14
+ })).catch((error) => {
15
+ res.statusCode = 500;
16
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
17
+ res.write(JSON.stringify(error));
18
+ res.end();
19
+ });
20
+ }).listen(port, () => {
21
+ console.info('ESM proxy server listining on', server.address());
22
+ resolve();
23
+ });
24
+ await promise;
25
+ return () => new Promise((resolve, reject) => {
26
+ console.info('closing the server');
27
+ server.close((error) => {
28
+ if (error)
29
+ reject(error);
30
+ else
31
+ resolve();
32
+ });
33
+ });
34
+ }
@@ -0,0 +1,26 @@
1
+ import { $, ProcessOutput } from 'zx';
2
+ import { glob } from 'glob';
3
+ import { rm } from 'node:fs/promises';
4
+ export async function unpublish({ registry, esmStoragePath, name, }) {
5
+ await Promise.all([
6
+ unpublishPackage(registry, name),
7
+ deleteESMCache(esmStoragePath, name),
8
+ ]);
9
+ }
10
+ async function unpublishPackage(registry, name) {
11
+ try {
12
+ await $ `npm unpublish --registry ${registry} --force ${name}`;
13
+ }
14
+ catch (error) {
15
+ if (error instanceof ProcessOutput &&
16
+ !error.stderr.includes('npm error code E404'))
17
+ throw error;
18
+ }
19
+ }
20
+ async function deleteESMCache(esmStoragePath, name) {
21
+ const paths = await glob(`${esmStoragePath}/**/${name}@*`);
22
+ await Promise.all(paths.map((path) => {
23
+ console.info('Deleting', path);
24
+ return rm(path, { recursive: true });
25
+ }));
26
+ }
@@ -0,0 +1,32 @@
1
+ import { setTimeout } from 'node:timers/promises';
2
+ export async function until({ interval, timeout, try: Try, }) {
3
+ const signal = AbortSignal.timeout(timeout);
4
+ while (!signal.aborted) {
5
+ try {
6
+ if (await Try(signal))
7
+ return true;
8
+ }
9
+ catch (error) { }
10
+ try {
11
+ await setTimeout(interval, null, { signal });
12
+ }
13
+ catch (error) { }
14
+ }
15
+ return false;
16
+ }
17
+ export async function waitForEndpoint({ interval = 300, timeout = 10_000, endpoint, }) {
18
+ if (!(await until({
19
+ interval,
20
+ timeout,
21
+ async try(signal) {
22
+ const response = await fetch(endpoint, { signal });
23
+ return response.ok;
24
+ },
25
+ })))
26
+ throw new EndpointUnavailableError();
27
+ }
28
+ export class EndpointUnavailableError extends Error {
29
+ constructor() {
30
+ super('Endpoint not available');
31
+ }
32
+ }
@@ -0,0 +1,88 @@
1
+ import { watch as fsWatch } from 'node:fs';
2
+ import { republish } from './republish.js';
3
+ import { getPackageMeta } from './getPackageMeta.js';
4
+ import { readFile, writeFile } from 'node:fs/promises';
5
+ import { createHash } from 'node:crypto';
6
+ import { glob } from 'glob';
7
+ import { queue, queuedDebounce } from './queue.js';
8
+ import { getWatchIgnorer } from './watchIgnoreList.js';
9
+ import { tempfile } from 'zx';
10
+ export async function watch(packagePath, opts) {
11
+ const { name, packageRoot, private: prvte, } = await getPackageMeta(packagePath);
12
+ if (prvte) {
13
+ console.info(`${name} is a private package... ignoring`);
14
+ return () => { };
15
+ }
16
+ console.info('Watching', packageRoot);
17
+ const abortController = new AbortController();
18
+ const { signal } = abortController;
19
+ const republishPackage = (filename = '') => {
20
+ console.info(`Change detected at ${packageRoot}/${filename}`);
21
+ return republish(packageRoot, opts).catch(console.error);
22
+ };
23
+ try {
24
+ await queue(republishPackage, signal);
25
+ }
26
+ catch (error) {
27
+ if (error.name === 'AbortError')
28
+ return () => { };
29
+ else
30
+ throw error;
31
+ }
32
+ return opts.legacyMethod
33
+ ? legacyWatch(abortController, packageRoot, republishPackage)
34
+ : modernWatch(abortController, packageRoot, queuedDebounce(republishPackage, 1_000, signal));
35
+ }
36
+ async function modernWatch(abortController, dirname, republish) {
37
+ abortController.signal.addEventListener('abort', () => {
38
+ console.info('Stop watching', dirname);
39
+ watcher.close();
40
+ });
41
+ const ignorer = await getWatchIgnorer(dirname);
42
+ const watcher = fsWatch(dirname, { recursive: true, signal: abortController.signal }, (_, filename) => (filename && ignorer.ignores(filename)) ||
43
+ republish(filename ?? '').catch((error) => {
44
+ if (!(error instanceof Event && error.target instanceof AbortSignal) &&
45
+ error.name !== 'AbortError')
46
+ throw error;
47
+ }));
48
+ return () => abortController.abort();
49
+ }
50
+ async function legacyWatch(abortController, dirname, republish) {
51
+ const ignorer = await getWatchIgnorer(dirname);
52
+ const filename = tempfile();
53
+ await hashDirectory(dirname, ignorer, filename, () => { });
54
+ abortController.signal.addEventListener('abort', () => {
55
+ console.info('Stop (legacy) watching', dirname);
56
+ clearTimeout(timer);
57
+ });
58
+ let timer;
59
+ beginTimer();
60
+ return () => abortController.abort();
61
+ function beginTimer() {
62
+ timer = setTimeout(() => hashDirectory(dirname, ignorer, filename, republish)
63
+ .catch(console.error)
64
+ .finally(beginTimer), 1_000);
65
+ }
66
+ }
67
+ async function hashDirectory(dirname, ignorer, filename, onChange) {
68
+ const files = await glob(`${dirname}/**/*`, {
69
+ nodir: true,
70
+ ignore: { ignored: (path) => ignorer.ignores(path.fullpath()) },
71
+ });
72
+ const hashes = await Promise.all(files.map(async (file) => {
73
+ const contents = await readFile(file);
74
+ return createHash('sha256').update(contents).digest('hex');
75
+ }));
76
+ const newHash = hashes.join('');
77
+ let previousHash = '';
78
+ try {
79
+ previousHash = await readFile(filename, 'utf-8');
80
+ }
81
+ catch (error) {
82
+ if (error.code !== 'ENOENT')
83
+ throw error;
84
+ }
85
+ if (newHash !== previousHash) {
86
+ await Promise.all([writeFile(filename, newHash), onChange()]);
87
+ }
88
+ }