All files / ts / publish / npm.ts

60.00% Branches 3/5
96.67% Lines 87/90
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
x1
 
 
x3
 
x3
x3
 
x3
x3
x3
x3
x3
 
 
x3
x3
x5
x5
x5
x5
x5
x5
x5
x5
x20
 
x5
x5
x5
x5
x5
x5
x5
x5
x5
x5
 
x5
x45
x19
x21
x21
x19
x31
x31
x19
 
x5
x5
x5
x5
x5
x5
x5
x5
x15
x5
x5
x5
x5
 
x35
x5
 
x3
x3
x3
x3
 
x4
x16
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x44
x44
x32
x4
x4
x4
x4
x4
x4
x4
x28
 
 
 
x4
x4
x4
x4
x20
x4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 



























































































I














































/**
 * Publish a TypeScript package on npm registries.
 * @module
 */

// Imports
import * as JSONC from "@std/jsonc"
import type { Arg, Optional, record } from "@libs/typing"
import { assertMatch } from "@std/assert"
import { Logger } from "@libs/logger"
import { bundle } from "../bundle.ts"
import { dirname, resolve, toFileUrl } from "@std/path"
import { command } from "@libs/run/command"
export type { Arg, Logger, record }

/** Transform a `deno.jsonc` file into a `package.json` and bundle exported entrypoints to make package publishable on json. */
export async function packaged(path = "deno.jsonc", { logger: log = new Logger(), scope = undefined as Optional<string>, name = undefined as Optional<string> } = {}): Promise<package_output> {
  path = resolve(path)
  log.debug(`processing: ${path}`)
  const mod = JSONC.parse(await Deno.readTextFile(path)) as record<string> & { exports?: record<string> }
  // Validate package name
  assertMatch(mod.name, /^@[a-z0-9-]+\/[a-z0-9-]+$/)
  const [_scope, _name] = mod.name.split("/")
  scope ??= _scope
  name ??= _name
  log = log.with({ scope, name }).debug()

  // Setup package.json
  const json = {
    name: `${scope}/${name}`,
    version: mod.version,
    type: "module",
    scripts: {},
    dependencies: {},
    devDependencies: {},
  } as package_output["json"]
  log.trace(`set version: ${json.version}`)

  // Copy optional fields
  for (const key of ["description", "keywords", "license", "author", "homepage", "repository", "funding"] as const) {
    if (mod[key]) {
      json[key] = mod[key]
      log.trace(`set ${key}: ${json[key]}`)
    } else {
      log.trace(`skipped ${key}`)
    }
  }

  // Copy exports
  const exports = {} as record<string>
  if (mod.exports) {
    json.exports = {}
    for (const [key, value] of Object.entries(mod.exports) as [string, string][]) {
      const url = toFileUrl(resolve(dirname(path), value))
      const file = value.replace(/\.ts$/, ".mjs")
      log.trace(`bundling: ${file}`)
      const code = await bundle(url, { config: toFileUrl(path) })
      json.exports[key] = file
      exports[file] = code
    }
  }

  return { directory: dirname(path), scope, name, json, exports } as package_output
}

/** Publish a TypeScript package on npm registries. */
export async function publish(
  path: Arg<typeof packaged>,
  { logger: log = new Logger(), registries = [], dryrun, provenance, ...options }: Arg<typeof packaged, 1> & { logger?: Logger; registries?: registry[]; dryrun?: boolean; provenance?: boolean },
): Promise<Pick<package_output, "scope" | "name" | "json">> {
  const { directory, scope, name, json, exports } = await packaged(path, options)
  log = log.with({ scope, name })
  log.debug("creating: package.json")
  await Deno.writeTextFile(resolve(directory, "package.json"), JSON.stringify(json, null, 2))
  for (const [key, value] of Object.entries(exports)) {
    log.debug(`creating export: ${key}`)
    await Deno.writeTextFile(resolve(directory, key), value)
  }
  const cwd = Deno.cwd()
  try {
    for (const { url, token, access } of registries) {
      Deno.chdir(directory)
      await command("npm", ["set", "--location", "project", "registry", url], { logger: log, winext: ".cmd", throw: true })
      await command("npm", ["set", "--location", "project", `//${new URL(url).host}/:_authToken`, token], { logger: log, winext: ".cmd", throw: true })
      const args = ["publish", "--access", { public: "public", private: "restricted" }[access]]
      if (dryrun) {
        args.push("--dry-run")
      }
      if (provenance) {
        args.push("--provenance")
      }
      log.debug(`publishing to: ${url} (${access})`)
      const { success, stdout, stderr } = await command("npm", args, { logger: log, env: { NPM_TOKEN: token }, winext: ".cmd" })
      if ((!success) && (!`${stdout}\n${stderr}`.includes("You cannot publish over the previously published versions"))) {
        throw new Error(`npm publish failed: ${stdout}\n${stderr}`)
      }
    }
  } finally {
    Deno.chdir(cwd)
  }
  return { scope, name, json }
}

/** Package output. */
export type package_output = {
  /** Package directory. */
  directory: string
  /** Package scope. */
  scope: string
  /** Package name. */
  name: string
  /** Content of translated package.json. */
  json: {
    name: string
    version: string
    type: "module"
    scripts: record<string>
    exports?: record<string>
    description?: string
    keywords?: string | string[]
    license?: string
    author?: string
    homepage?: string
    repository?: string
    funding?: string
    dependencies: record<string>
    devDependencies?: record<string>
  }
  /** Exported entrypoints. */
  exports: record<string>
}

/** Registry configuration. */
export type registry = {
  /** Registry URL. */
  url: string
  /** Registry publishing token. */
  token: string
  /** Registry publishing access level. */
  access: "public" | "private"
}