esm.dev 1.6.0 → 1.7.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.
@@ -1,15 +1,24 @@
1
- import * as path from 'node:path';
2
1
  import { ESMDevCommand } from './ESMDevCommand.js';
3
2
  import { PackagePathSpecific } from './mixins/PackagePathSpecific.js';
4
3
  import { MustacheGeneratorCommand } from './MustacheGeneratorCommand.js';
5
4
  import { Option } from 'clipanion';
5
+ import { isNumber } from 'typanion';
6
6
  export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
7
7
  static paths = [['init']];
8
8
  static usage = ESMDevCommand.Usage({
9
9
  description: 'Initialises your repo ready to work with ESM.sh locally',
10
+ examples: [
11
+ ['Create a minimal template', 'esm.dev init'],
12
+ ["Specify the packages you're developing", 'esm.dev init packages/*'],
13
+ [
14
+ 'Specify different ports',
15
+ 'esm.dev init --port 3001 --esm-port 8081 --registry-port 4444',
16
+ ],
17
+ ],
10
18
  });
11
- esmOrigin = Option.String('-e,--esm-origin', 'http://localhost:8080', {
12
- description: 'The base URL of the ESM Server',
19
+ esmPort = Option.String('-e,--esm-port', '8080', {
20
+ description: 'The port of the ESM Server',
21
+ validator: isNumber(),
13
22
  });
14
23
  esmStoragePath = Option.String('-s,--esm-storage-path', './docker-storage/esm/esmd', {
15
24
  description: "Path to ESM.sh's storage",
@@ -20,17 +29,17 @@ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
20
29
  outputDirectory = Option.String('-o,--output-dir', '.', {
21
30
  description: 'The output directory',
22
31
  });
23
- registry = Option.String('-r,--registry', 'http://localhost:4873', {
24
- description: 'The URL of your local registry',
32
+ registryPort = Option.String('-r,--registry-port', '4873', {
33
+ description: 'The port of your local registry',
34
+ validator: isNumber(),
25
35
  });
26
36
  esmURL;
27
37
  npmRegistryURL;
28
38
  packages;
29
39
  async execute() {
40
+ const path = await import('node:path');
30
41
  this.templateDir = path.resolve(import.meta.dirname, '..', '..', 'templates', 'init');
31
42
  this.destinationDir = this.outputDirectory;
32
- this.esmURL = new URL(this.esmOrigin);
33
- this.npmRegistryURL = new URL(this.registry);
34
43
  this.packages = this.packagePaths.map((packagePath) => ({
35
44
  path: packagePath,
36
45
  basename: path.basename(packagePath),
package/dist/lib/queue.js CHANGED
@@ -1,12 +1,19 @@
1
1
  import debounce from 'lodash.debounce';
2
2
  import throat from 'throat';
3
- export const queue = throat.default(1);
3
+ export async function queue(signal, fn) {
4
+ return _queue((...args) => {
5
+ signal.throwIfAborted();
6
+ return fn(...args);
7
+ });
8
+ }
9
+ const _queue = throat.default(1);
4
10
  /**
5
11
  * Just like debounce, but the first call will add a promise to the queue
6
12
  * and that promise won't resolve until the debounced function is finally called.
7
13
  */
8
- export function queuedDebounce(fn, delay) {
14
+ export function queuedDebounce(fn, delay, signal) {
9
15
  let promiseWithResolvers;
16
+ signal.addEventListener('abort', (reason) => promiseWithResolvers?.reject(reason));
10
17
  const debounced = debounce(async (...args) => {
11
18
  try {
12
19
  const result = await fn(...args);
@@ -20,14 +27,15 @@ export function queuedDebounce(fn, delay) {
20
27
  }
21
28
  }, delay);
22
29
  return (...args) => {
23
- start();
30
+ const { promise } = start();
24
31
  debounced(...args);
25
- return promiseWithResolvers.promise;
32
+ return promise;
26
33
  };
27
34
  function start() {
28
35
  if (!promiseWithResolvers) {
29
36
  promiseWithResolvers = Promise.withResolvers();
30
- queue(async () => promiseWithResolvers?.promise);
37
+ queue(signal, async () => promiseWithResolvers?.promise);
31
38
  }
39
+ return promiseWithResolvers;
32
40
  }
33
41
  }
@@ -1,14 +1,19 @@
1
1
  import { queue } from './queue.js';
2
2
  import { createServer } from 'node:http';
3
3
  import httpProxy from 'http-proxy';
4
+ import { addAbortSignal } from 'node:stream';
4
5
  export function serve(port, esmOrigin) {
5
6
  const proxy = httpProxy.createProxyServer();
7
+ const abortController = new AbortController();
8
+ const signal = abortController.signal;
6
9
  const server = createServer((req, res) => {
7
- queue(() => new Promise((resolve, reject) => {
10
+ queue(signal, () => new Promise((resolve, reject) => {
8
11
  console.info('Proxying', req.url);
12
+ addAbortSignal(signal, req);
13
+ addAbortSignal(signal, res);
14
+ req.on('error', reject);
9
15
  res.on('error', reject);
10
16
  res.on('close', resolve);
11
- res.on('finish', resolve);
12
17
  proxy.web(req, res, { target: esmOrigin });
13
18
  })).catch((error) => {
14
19
  res.statusCode = 500;
@@ -19,5 +24,10 @@ export function serve(port, esmOrigin) {
19
24
  }).listen(port, () => {
20
25
  console.info('ESM proxy server listining on', server.address());
21
26
  });
22
- return server;
27
+ return () => {
28
+ console.info('closing the server');
29
+ abortController.abort();
30
+ server.closeAllConnections();
31
+ server.close();
32
+ };
23
33
  }
package/dist/lib/watch.js CHANGED
@@ -12,18 +12,29 @@ export async function watch(packagePath, opts) {
12
12
  return () => { };
13
13
  }
14
14
  console.info('Watching', packagePath);
15
+ const abortController = new AbortController();
16
+ const signal = abortController.signal;
15
17
  const republishPackage = () => republish(packagePath, opts).catch(console.error);
16
18
  const debounceRepublish = queuedDebounce((filename = '') => {
17
19
  console.info(`Change detected at ${packagePath}/${filename}`);
18
20
  return republishPackage();
19
- }, 1_000);
20
- await queue(republishPackage);
21
+ }, 1_000, signal);
22
+ await queue(signal, republishPackage);
21
23
  if (opts.legacyMethod) {
22
- return await legacyWatch(packagePath, () => debounceRepublish());
24
+ const close = await legacyWatch(packagePath, () => debounceRepublish());
25
+ return () => {
26
+ console.info('stop watching');
27
+ close();
28
+ abortController.abort();
29
+ };
23
30
  }
24
31
  else {
25
32
  const watcher = fsWatch(packageRoot, { recursive: true }, (_, filename) => debounceRepublish(filename ?? ''));
26
- return () => watcher.close();
33
+ return () => {
34
+ console.info('stop watching');
35
+ watcher.close();
36
+ abortController.abort();
37
+ };
27
38
  }
28
39
  }
29
40
  async function legacyWatch(dirname, cb) {
@@ -0,0 +1,24 @@
1
+ group "default" {
2
+ targets = ["app"]
3
+ }
4
+
5
+ variable "TAG" {
6
+ validation {
7
+ condition = TAG == regex("^(?:\\d+\\.\\d+\\.\\d+)?$", TAG)
8
+ error_message = "The variable 'TAG' has to be in the 'x.x.x' format."
9
+ }
10
+ }
11
+
12
+ variable "IMAGE" {
13
+ default = "ghcr.io/johngeorgewright/esm.dev"
14
+ }
15
+
16
+ target "app" {
17
+ context = "."
18
+ dockerfile = "Dockerfile"
19
+ platforms = ["linux/amd64", "linux/arm64"]
20
+ tags = [
21
+ "${IMAGE}:latest",
22
+ notequal("", TAG) ? "${IMAGE}:${TAG}" : ""
23
+ ]
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm.dev",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "TypeScript library template",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,23 +1,28 @@
1
- import * as path from 'node:path'
2
1
  import { ESMDevCommand } from './ESMDevCommand.js'
3
2
  import { PackagePathSpecific } from './mixins/PackagePathSpecific.js'
4
3
  import { MustacheGeneratorCommand } from './MustacheGeneratorCommand.js'
5
4
  import { Option } from 'clipanion'
5
+ import { isNumber } from 'typanion'
6
6
 
7
7
  export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
8
8
  static override paths = [['init']]
9
9
 
10
10
  static override usage = ESMDevCommand.Usage({
11
11
  description: 'Initialises your repo ready to work with ESM.sh locally',
12
+ examples: [
13
+ ['Create a minimal template', 'esm.dev init'],
14
+ ["Specify the packages you're developing", 'esm.dev init packages/*'],
15
+ [
16
+ 'Specify different ports',
17
+ 'esm.dev init --port 3001 --esm-port 8081 --registry-port 4444',
18
+ ],
19
+ ],
12
20
  })
13
21
 
14
- readonly esmOrigin = Option.String(
15
- '-e,--esm-origin',
16
- 'http://localhost:8080',
17
- {
18
- description: 'The base URL of the ESM Server',
19
- },
20
- )
22
+ readonly esmPort = Option.String('-e,--esm-port', '8080', {
23
+ description: 'The port of the ESM Server',
24
+ validator: isNumber(),
25
+ })
21
26
 
22
27
  readonly esmStoragePath = Option.String(
23
28
  '-s,--esm-storage-path',
@@ -35,8 +40,9 @@ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
35
40
  description: 'The output directory',
36
41
  })
37
42
 
38
- readonly registry = Option.String('-r,--registry', 'http://localhost:4873', {
39
- description: 'The URL of your local registry',
43
+ readonly registryPort = Option.String('-r,--registry-port', '4873', {
44
+ description: 'The port of your local registry',
45
+ validator: isNumber(),
40
46
  })
41
47
 
42
48
  esmURL!: URL
@@ -44,6 +50,7 @@ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
44
50
  packages!: { path: string; basename: string }[]
45
51
 
46
52
  override async execute() {
53
+ const path = await import('node:path')
47
54
  this.templateDir = path.resolve(
48
55
  import.meta.dirname,
49
56
  '..',
@@ -52,8 +59,6 @@ export class InitCommand extends PackagePathSpecific(MustacheGeneratorCommand) {
52
59
  'init',
53
60
  )
54
61
  this.destinationDir = this.outputDirectory
55
- this.esmURL = new URL(this.esmOrigin)
56
- this.npmRegistryURL = new URL(this.registry)
57
62
  this.packages = this.packagePaths.map((packagePath) => ({
58
63
  path: packagePath,
59
64
  basename: path.basename(packagePath),
package/src/lib/queue.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import debounce from 'lodash.debounce'
2
2
  import throat from 'throat'
3
3
 
4
- export const queue = throat.default(1)
4
+ export async function queue<TResult, TArgs extends any[] = []>(
5
+ signal: AbortSignal,
6
+ fn: (...args: TArgs) => Promise<TResult>,
7
+ ): Promise<TResult> {
8
+ return _queue((...args) => {
9
+ signal.throwIfAborted()
10
+ return fn(...(args as any))
11
+ })
12
+ }
13
+
14
+ const _queue = throat.default(1)
5
15
 
6
16
  /**
7
17
  * Just like debounce, but the first call will add a promise to the queue
@@ -10,9 +20,14 @@ export const queue = throat.default(1)
10
20
  export function queuedDebounce<Args extends unknown[], R>(
11
21
  fn: (...args: Args) => R,
12
22
  delay: number,
23
+ signal: AbortSignal,
13
24
  ): (...args: Args) => Promise<R> {
14
25
  let promiseWithResolvers: PromiseWithResolvers<R> | undefined
15
26
 
27
+ signal.addEventListener('abort', (reason) =>
28
+ promiseWithResolvers?.reject(reason),
29
+ )
30
+
16
31
  const debounced = debounce(async (...args: Args) => {
17
32
  try {
18
33
  const result = await fn(...args)
@@ -25,15 +40,16 @@ export function queuedDebounce<Args extends unknown[], R>(
25
40
  }, delay)
26
41
 
27
42
  return (...args: Args) => {
28
- start()
43
+ const { promise } = start()
29
44
  debounced(...args)
30
- return promiseWithResolvers!.promise
45
+ return promise
31
46
  }
32
47
 
33
48
  function start() {
34
49
  if (!promiseWithResolvers) {
35
50
  promiseWithResolvers = Promise.withResolvers<R>()
36
- queue(async () => promiseWithResolvers?.promise)
51
+ queue(signal, async () => promiseWithResolvers?.promise)
37
52
  }
53
+ return promiseWithResolvers
38
54
  }
39
55
  }
package/src/lib/server.ts CHANGED
@@ -1,18 +1,24 @@
1
1
  import { queue } from './queue.js'
2
2
  import { createServer } from 'node:http'
3
3
  import httpProxy from 'http-proxy'
4
+ import { addAbortSignal } from 'node:stream'
4
5
 
5
6
  export function serve(port: number, esmOrigin: string) {
6
7
  const proxy = httpProxy.createProxyServer()
8
+ const abortController = new AbortController()
9
+ const signal = abortController.signal
7
10
 
8
11
  const server = createServer((req, res) => {
9
12
  queue(
13
+ signal,
10
14
  () =>
11
15
  new Promise<void>((resolve, reject) => {
12
16
  console.info('Proxying', req.url)
17
+ addAbortSignal(signal, req)
18
+ addAbortSignal(signal, res)
19
+ req.on('error', reject)
13
20
  res.on('error', reject)
14
21
  res.on('close', resolve)
15
- res.on('finish', resolve)
16
22
  proxy.web(req, res, { target: esmOrigin })
17
23
  }),
18
24
  ).catch((error) => {
@@ -25,5 +31,10 @@ export function serve(port: number, esmOrigin: string) {
25
31
  console.info('ESM proxy server listining on', server.address())
26
32
  })
27
33
 
28
- return server
34
+ return () => {
35
+ console.info('closing the server')
36
+ abortController.abort()
37
+ server.closeAllConnections()
38
+ server.close()
39
+ }
29
40
  }
package/src/lib/watch.ts CHANGED
@@ -23,23 +23,39 @@ export async function watch(
23
23
 
24
24
  console.info('Watching', packagePath)
25
25
 
26
+ const abortController = new AbortController()
27
+ const signal = abortController.signal
28
+
26
29
  const republishPackage = () =>
27
30
  republish(packagePath, opts).catch(console.error)
28
31
 
29
- const debounceRepublish = queuedDebounce((filename: string = '') => {
30
- console.info(`Change detected at ${packagePath}/${filename}`)
31
- return republishPackage()
32
- }, 1_000)
32
+ const debounceRepublish = queuedDebounce(
33
+ (filename: string = '') => {
34
+ console.info(`Change detected at ${packagePath}/${filename}`)
35
+ return republishPackage()
36
+ },
37
+ 1_000,
38
+ signal,
39
+ )
33
40
 
34
- await queue(republishPackage)
41
+ await queue(signal, republishPackage)
35
42
 
36
43
  if (opts.legacyMethod) {
37
- return await legacyWatch(packagePath, () => debounceRepublish())
44
+ const close = await legacyWatch(packagePath, () => debounceRepublish())
45
+ return () => {
46
+ console.info('stop watching')
47
+ close()
48
+ abortController.abort()
49
+ }
38
50
  } else {
39
51
  const watcher = fsWatch(packageRoot, { recursive: true }, (_, filename) =>
40
52
  debounceRepublish(filename ?? ''),
41
53
  )
42
- return () => watcher.close()
54
+ return () => {
55
+ console.info('stop watching')
56
+ watcher.close()
57
+ abortController.abort()
58
+ }
43
59
  }
44
60
  }
45
61
 
@@ -5,8 +5,8 @@ services:
5
5
  - esm
6
6
  - npm
7
7
  environment:
8
- - NPM_REGISTRY=http://npm:{{npmRegistryURL.port}}
9
- - ESM_ORIGIN=http://esm:{{esmURL.port}}
8
+ - NPM_REGISTRY=http://npm:{{registryPort}}
9
+ - ESM_ORIGIN=http://esm:{{esmPort}}
10
10
  - ESM_STORAGE_PATH=/esmd
11
11
  - PORT={{port}}
12
12
  command:
@@ -20,19 +20,23 @@ services:
20
20
  {{#packages}}
21
21
  - {{path}}:/watch/{{basename}}:ro
22
22
  {{/packages}}
23
+ {{^packages}}
24
+ # EG:
25
+ # - ./packages/*:/watch:ro
26
+ {{/packages}}
23
27
 
24
28
  esm:
25
29
  image: ghcr.io/esm-dev/esm.sh:latest
26
30
  environment:
27
- - NPM_REGISTRY=http://npm:{{npmRegistryURL.port}}
31
+ - NPM_REGISTRY=http://npm:{{registryPort}}
28
32
  - NPM_TOKEN=fake
29
33
  - LOG_LEVEL=debug
30
34
  ports:
31
- - '{{esmURL.port}}:8080'
35
+ - '{{esmPort}}:8080'
32
36
  volumes:
33
37
  - {{esmStoragePath}}:/esmd
34
38
 
35
39
  npm:
36
40
  image: verdaccio/verdaccio:latest
37
41
  ports:
38
- - '{{npmRegistryURL.port}}:4873'
42
+ - '{{registryPort}}:4873'