esm.dev 1.5.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 (65) hide show
  1. package/.prettierignore +4 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/commitlint.config.cjs +8 -0
  5. package/dist/cli.js +17 -0
  6. package/dist/commands/ESMDevCommand.js +6 -0
  7. package/dist/commands/InitCommand.js +40 -0
  8. package/dist/commands/LoginCommand.js +12 -0
  9. package/dist/commands/MustacheGeneratorCommand.js +9 -0
  10. package/dist/commands/RepublishCommand.js +11 -0
  11. package/dist/commands/StartCommand.js +16 -0
  12. package/dist/commands/TokenCommand.js +20 -0
  13. package/dist/commands/WaitForRegistryCommand.js +12 -0
  14. package/dist/commands/WatchCommand.js +12 -0
  15. package/dist/commands/mixins/CommandClass.js +1 -0
  16. package/dist/commands/mixins/ESMServed.js +9 -0
  17. package/dist/commands/mixins/ESMStorageSpecific.js +9 -0
  18. package/dist/commands/mixins/PackagePathSpecific.js +14 -0
  19. package/dist/commands/mixins/RegistrySpecific.js +9 -0
  20. package/dist/commands/mixins/Retryable.js +28 -0
  21. package/dist/commands/mixins/Servable.js +11 -0
  22. package/dist/commands/mixins/Watchable.js +9 -0
  23. package/dist/lib/MustacheGenerator.js +9 -0
  24. package/dist/lib/getPackageMeta.js +10 -0
  25. package/dist/lib/login.js +31 -0
  26. package/dist/lib/publish.js +5 -0
  27. package/dist/lib/queue.js +33 -0
  28. package/dist/lib/republish.js +9 -0
  29. package/dist/lib/server.js +23 -0
  30. package/dist/lib/unpublish.js +25 -0
  31. package/dist/lib/until.js +16 -0
  32. package/dist/lib/watch.js +61 -0
  33. package/dist/options/EnvOption.js +6 -0
  34. package/package.json +66 -0
  35. package/release.config.cjs +20 -0
  36. package/src/cli.ts +18 -0
  37. package/src/commands/ESMDevCommand.ts +8 -0
  38. package/src/commands/InitCommand.ts +57 -0
  39. package/src/commands/LoginCommand.ts +15 -0
  40. package/src/commands/MustacheGeneratorCommand.ts +10 -0
  41. package/src/commands/RepublishCommand.ts +15 -0
  42. package/src/commands/StartCommand.ts +21 -0
  43. package/src/commands/TokenCommand.ts +26 -0
  44. package/src/commands/WaitForRegistryCommand.ts +17 -0
  45. package/src/commands/WatchCommand.ts +15 -0
  46. package/src/commands/mixins/CommandClass.ts +3 -0
  47. package/src/commands/mixins/ESMServed.ts +12 -0
  48. package/src/commands/mixins/ESMStorageSpecific.ts +16 -0
  49. package/src/commands/mixins/PackagePathSpecific.ts +22 -0
  50. package/src/commands/mixins/RegistrySpecific.ts +12 -0
  51. package/src/commands/mixins/Retryable.ts +35 -0
  52. package/src/commands/mixins/Servable.ts +14 -0
  53. package/src/commands/mixins/Watchable.ts +13 -0
  54. package/src/commands/templates/init/docker-compose.yml.mustache +38 -0
  55. package/src/lib/MustacheGenerator.ts +10 -0
  56. package/src/lib/getPackageMeta.ts +15 -0
  57. package/src/lib/login.ts +35 -0
  58. package/src/lib/publish.ts +14 -0
  59. package/src/lib/queue.ts +39 -0
  60. package/src/lib/republish.ts +16 -0
  61. package/src/lib/server.ts +29 -0
  62. package/src/lib/unpublish.ts +43 -0
  63. package/src/lib/until.ts +22 -0
  64. package/src/lib/watch.ts +84 -0
  65. package/src/options/EnvOption.ts +27 -0
@@ -0,0 +1,4 @@
1
+ dist
2
+ *.log
3
+ .pnp.cjs
4
+ .yarn/*
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 John Wright
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # esm.dev
2
+
3
+ A set of utils when working with local NPM packages and [esm.sh](https://esm.sh/).
4
+
5
+ It expects you to have a local version of ESM.sh and verdaccio running. It will monitor changes in a configurable set of directories. When it sees any changes, it will unpublish from verdaccio, remove builds & cache from ESM.sh and then re-publish the package.
6
+
7
+ ## Usage
8
+
9
+ The simplest solution is to use docker-compose. Using the configuration below:
10
+
11
+ `docker compose up`
12
+
13
+ ```yaml
14
+ services:
15
+ esm.dev:
16
+ image: ghcr.io/johngeorgewright/esm.dev:latest
17
+ depends_on:
18
+ - esm.sh
19
+ - npm
20
+ environment:
21
+ - NPM_REGISTRY=http://npm:4873
22
+ - ESM_ORIGIN=http://esm.sh:8080
23
+ - ESM_STORAGE_PATH=/esmd
24
+ - PORT=3000
25
+ command:
26
+ - start
27
+ - /watch/*
28
+ ports:
29
+ - '3000:3000'
30
+ volumes:
31
+ - ./docker-storage/esm/esmd:/esmd
32
+ # The following are paths to all the packages you wish to auto publish
33
+ - ./packages/package-1:/watch/package-1:ro
34
+ - ./packages/package-2:/watch/package-2:ro
35
+
36
+ esm.sh:
37
+ image: ghcr.io/esm-dev/esm.sh:latest
38
+ environment:
39
+ - NPM_REGISTRY=http://npm:4873
40
+ - NPM_TOKEN=fake
41
+ - LOG_LEVEL=debug
42
+ ports:
43
+ - '8080:8080'
44
+ volumes:
45
+ - ./docker-storage/esm/esmd:/esmd
46
+
47
+ npm:
48
+ image: verdaccio/verdaccio:latest
49
+ ports:
50
+ - '4873:4873'
51
+ ```
52
+
53
+ Once the above is running point your ESM modules to `localhost:3000`:
54
+
55
+ ```html
56
+ <script type="importmap">
57
+ {
58
+ "imports": {
59
+ "react": "https://esm.sh/react",
60
+ "package-1": "http://localhost:3000/package-1"
61
+ }
62
+ }
63
+ </script>
64
+ <script type="module">
65
+ import Package1 from 'package1'
66
+ // ...
67
+ </script>
68
+ ```
@@ -0,0 +1,8 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @type {import('@commitlint/types').UserConfig}
5
+ */
6
+ module.exports = {
7
+ extends: ['@commitlint/config-conventional'],
8
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,17 @@
1
+ import { runExit } from 'clipanion';
2
+ import { WatchCommand } from './commands/WatchCommand.js';
3
+ import { RepublishCommand } from './commands/RepublishCommand.js';
4
+ import { LoginCommand } from './commands/LoginCommand.js';
5
+ import { TokenCommand } from './commands/TokenCommand.js';
6
+ import { WaitForRegistryCommand } from './commands/WaitForRegistryCommand.js';
7
+ import { StartCommand } from './commands/StartCommand.js';
8
+ import { InitCommand } from './commands/InitCommand.js';
9
+ runExit([
10
+ InitCommand,
11
+ LoginCommand,
12
+ TokenCommand,
13
+ RepublishCommand,
14
+ StartCommand,
15
+ WaitForRegistryCommand,
16
+ WatchCommand,
17
+ ]);
@@ -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,40 @@
1
+ import * as path from 'node:path';
2
+ import { ESMDevCommand } from './ESMDevCommand';
3
+ import { PackagePathSpecific } from './mixins/PackagePathSpecific';
4
+ import { MustacheGeneratorCommand } from './MustacheGeneratorCommand';
5
+ import { Option } from 'clipanion';
6
+ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
7
+ static paths = [['init']];
8
+ static usage = ESMDevCommand.Usage({
9
+ description: 'Initialises your repo ready to work with ESM.sh locally',
10
+ });
11
+ esmOrigin = Option.String('-e,--esm-origin', 'http://localhost:8080', {
12
+ description: 'The base URL of the ESM Server',
13
+ });
14
+ esmStoragePath = Option.String('-s,--esm-storage-path', './docker-storage/esm/esmd', {
15
+ description: "Path to ESM.sh's storage",
16
+ });
17
+ port = Option.String('-p,--port', '3000', {
18
+ description: "The server's port",
19
+ });
20
+ outputDirectory = Option.String('-o,--output-dir', '.', {
21
+ description: 'The output directory',
22
+ });
23
+ registry = Option.String('-r,--registry', 'http://localhost:4873', {
24
+ description: 'The URL of your local registry',
25
+ });
26
+ esmURL;
27
+ npmRegistryURL;
28
+ packages;
29
+ async execute() {
30
+ this.templateDir = path.join(import.meta.dirname, 'templates', 'init');
31
+ this.destinationDir = this.outputDirectory;
32
+ this.esmURL = new URL(this.esmOrigin);
33
+ this.npmRegistryURL = new URL(this.registry);
34
+ this.packages = this.packagePaths.map((packagePath) => ({
35
+ path: packagePath,
36
+ basename: path.basename(packagePath),
37
+ }));
38
+ await super.execute();
39
+ }
40
+ }
@@ -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 = Command.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';
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 = ESMDevCommand.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,16 @@
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 = ESMDevCommand.Usage({
8
+ description: 'Runs a server and concurrently watches a file system',
9
+ });
10
+ async execute() {
11
+ const { watch } = await import('../lib/watch.js');
12
+ const { serve } = await import('../lib/server.js');
13
+ serve(this.port, this.esmOrigin);
14
+ await this.eachPackagePath((packagePath) => watch(packagePath, this));
15
+ }
16
+ }
@@ -0,0 +1,20 @@
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 = Command.Usage({
6
+ description: 'Get the NPM token',
7
+ });
8
+ async execute() {
9
+ const { default: escapeStringRegexp } = await import('escape-string-regexp');
10
+ const npmrc = await Bun.file(`~/.npmrc`).text();
11
+ const url = new URL(this.registry);
12
+ const regex = new RegExp(`^//${escapeStringRegexp(url.hostname)}/:_authToken=(.*)$`, 'm');
13
+ const match = regex.exec(npmrc);
14
+ if (!match) {
15
+ this.context.stderr.write(`Token has not been created yet\n`);
16
+ return 1;
17
+ }
18
+ this.context.stdout.write(match[1] + '\n');
19
+ }
20
+ }
@@ -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 = Command.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 = ESMDevCommand.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';
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 Promise.all(this.packagePaths.map((packagePath) => glob(packagePath)))).flat();
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,28 @@
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 { until } = await import('../../lib/until.js');
15
+ if (!(await until({
16
+ ...this,
17
+ try: async (signal) => {
18
+ const response = await fetch(endpoint, { signal });
19
+ return response.ok;
20
+ },
21
+ }))) {
22
+ this.context.stderr.write(`${endpoint} is not available\n`);
23
+ return 1;
24
+ }
25
+ }
26
+ }
27
+ return Retryable;
28
+ }
@@ -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,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,31 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function login(registry) {
3
+ return new Promise((resolve) => {
4
+ const child = spawn('bunx', [
5
+ 'npm',
6
+ 'login',
7
+ '--registry',
8
+ registry,
9
+ '--quiet',
10
+ ]);
11
+ child.stderr.on('data', (d) => console.error(d.toString()));
12
+ child.stdout.on('data', (d) => {
13
+ const data = d.toString();
14
+ switch (true) {
15
+ case /username/i.test(data):
16
+ child.stdin.write('esm.dev\n');
17
+ break;
18
+ case /password/i.test(data):
19
+ child.stdin.write('esm.dev\n');
20
+ break;
21
+ case /email/i.test(data):
22
+ child.stdin.write('esm.dev@esm.dev.com');
23
+ break;
24
+ case /logged in as/i.test(data):
25
+ child.stdin.end();
26
+ break;
27
+ }
28
+ });
29
+ child.on('exit', resolve);
30
+ });
31
+ }
@@ -0,0 +1,5 @@
1
+ import { $ } from 'bun';
2
+ import * as path from 'node:path';
3
+ export async function publish({ packageRoot, registry, }) {
4
+ await $ `bunx npm publish --registry ${registry}`.cwd(path.resolve(packageRoot));
5
+ }
@@ -0,0 +1,33 @@
1
+ import debounce from 'lodash.debounce';
2
+ import throat from 'throat';
3
+ export const queue = throat(1);
4
+ /**
5
+ * Just like debounce, but the first call will add a promise to the queue
6
+ * and that promise won't resolve until the debounced function is finally called.
7
+ */
8
+ export function queuedDebounce(fn, delay) {
9
+ let promiseWithResolvers;
10
+ const debounced = debounce(async (...args) => {
11
+ try {
12
+ const result = await fn(...args);
13
+ promiseWithResolvers?.resolve(result);
14
+ }
15
+ catch (error) {
16
+ promiseWithResolvers?.reject(error);
17
+ }
18
+ finally {
19
+ promiseWithResolvers = undefined;
20
+ }
21
+ }, delay);
22
+ return (...args) => {
23
+ start();
24
+ debounced(...args);
25
+ return promiseWithResolvers.promise;
26
+ };
27
+ function start() {
28
+ if (!promiseWithResolvers) {
29
+ promiseWithResolvers = Promise.withResolvers();
30
+ queue(async () => promiseWithResolvers?.promise);
31
+ }
32
+ }
33
+ }
@@ -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,23 @@
1
+ import { queue } from './queue.js';
2
+ import { createServer } from 'node:http';
3
+ import httpProxy from 'http-proxy';
4
+ export function serve(port, esmOrigin) {
5
+ const proxy = httpProxy.createProxyServer();
6
+ const server = createServer((req, res) => {
7
+ queue(() => new Promise((resolve, reject) => {
8
+ console.info('Proxying', req.url);
9
+ res.on('error', reject);
10
+ res.on('close', resolve);
11
+ res.on('finish', resolve);
12
+ proxy.web(req, res, { target: esmOrigin });
13
+ })).catch((error) => {
14
+ res.statusCode = 500;
15
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
16
+ res.write(JSON.stringify(error));
17
+ res.end();
18
+ });
19
+ }).listen(port, () => {
20
+ console.info('ESM proxy server listining on', server.address());
21
+ });
22
+ return server;
23
+ }
@@ -0,0 +1,25 @@
1
+ import { $ } from 'bun';
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 $ `bunx npm unpublish --registry ${registry} --force ${name}`;
13
+ }
14
+ catch (error) {
15
+ if (!(error instanceof $.ShellError &&
16
+ error.stderr.includes('npm error code E404')))
17
+ throw error;
18
+ }
19
+ }
20
+ async function deleteESMCache(esmStoragePath, name) {
21
+ await glob(`${esmStoragePath}/**/${name}@*`).then((paths) => Promise.all(paths.map((path) => {
22
+ console.info('Deleting', path);
23
+ return rm(path, { recursive: true });
24
+ })));
25
+ }
@@ -0,0 +1,16 @@
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, { signal });
12
+ }
13
+ catch (error) { }
14
+ }
15
+ return false;
16
+ }
@@ -0,0 +1,61 @@
1
+ import { watch as fsWatch, watchFile } 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
+ export async function watch(packagePath, opts) {
9
+ const { name, packageRoot, private: prvte, } = await getPackageMeta(packagePath);
10
+ if (prvte) {
11
+ console.info(`${name} is a private package... ignoring`);
12
+ return () => { };
13
+ }
14
+ console.info('Watching', packagePath);
15
+ const republishPackage = () => republish(packagePath, opts).catch(console.error);
16
+ const debounceRepublish = queuedDebounce((filename = '') => {
17
+ console.info(`Change detected at ${packagePath}/${filename}`);
18
+ return republishPackage();
19
+ }, 1_000);
20
+ await queue(republishPackage);
21
+ if (opts.legacyMethod) {
22
+ return await legacyWatch(packagePath, () => debounceRepublish());
23
+ }
24
+ else {
25
+ const watcher = fsWatch(packageRoot, { recursive: true }, (_, filename) => debounceRepublish(filename ?? ''));
26
+ return () => watcher.close();
27
+ }
28
+ }
29
+ async function legacyWatch(dirname, cb) {
30
+ const filename = await hashDirectory(dirname);
31
+ const watcher = watchFile(filename, cb);
32
+ let timer;
33
+ beginTimer();
34
+ return () => {
35
+ watcher.unref();
36
+ clearTimeout(timer);
37
+ };
38
+ function beginTimer() {
39
+ timer = setTimeout(() => hashDirectory(dirname).catch(console.error).finally(beginTimer), 1_000);
40
+ }
41
+ }
42
+ async function hashDirectory(dirname) {
43
+ const filename = `/tmp/${dirname.replace(/\//g, '-')}.hash.txt`;
44
+ const files = await glob(`${dirname}/**/*`, { nodir: true });
45
+ const hashes = await Promise.all(files.map(async (file) => {
46
+ const contents = await readFile(file, 'utf-8');
47
+ return createHash('sha256').update(contents).digest('hex');
48
+ }));
49
+ const newHash = hashes.join('');
50
+ let previousHash = '';
51
+ try {
52
+ previousHash = await readFile(filename, 'utf-8');
53
+ }
54
+ catch (error) {
55
+ if (error.code !== 'ENOENT')
56
+ throw error;
57
+ }
58
+ if (newHash !== previousHash)
59
+ await writeFile(filename, newHash);
60
+ return filename;
61
+ }
@@ -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
+ }