skyltmax_config 0.0.8 → 0.0.9

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.
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "node:fs"
4
+ import { readFile } from "node:fs/promises"
5
+ import { createRequire } from "node:module"
6
+ import { dirname, resolve } from "node:path"
7
+ import { fileURLToPath } from "node:url"
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = dirname(__filename)
11
+ const defaultManifestPath = resolve(__dirname, "..", "..", "package.json")
12
+ const defaultConsumerRoot = process.env.INIT_CWD ?? process.env.npm_config_local_prefix ?? process.cwd()
13
+
14
+ async function readManifest(path) {
15
+ const raw = await readFile(path, "utf8")
16
+ return JSON.parse(raw)
17
+ }
18
+
19
+ function createConsumerRequire(root) {
20
+ const candidate = resolve(root, "package.json")
21
+ if (existsSync(candidate)) {
22
+ return createRequire(candidate)
23
+ }
24
+
25
+ return createRequire(import.meta.url)
26
+ }
27
+
28
+ export async function auditPeerDependencies({
29
+ manifestPath = defaultManifestPath,
30
+ consumerRoot = defaultConsumerRoot,
31
+ } = {}) {
32
+ const manifest = await readManifest(manifestPath)
33
+ const peers = Object.entries(manifest.peerDependencies ?? {})
34
+
35
+ if (!peers.length) {
36
+ return {
37
+ peers,
38
+ missing: [],
39
+ mismatched: [],
40
+ }
41
+ }
42
+
43
+ const requireFromConsumer = createConsumerRequire(consumerRoot)
44
+ const missing = []
45
+ const mismatched = []
46
+
47
+ for (const [name, expectedVersion] of peers) {
48
+ let resolvedPackageJson
49
+ try {
50
+ resolvedPackageJson = requireFromConsumer.resolve(`${name}/package.json`)
51
+ } catch (error) {
52
+ missing.push({ name, expectedVersion, reason: error.message })
53
+ continue
54
+ }
55
+
56
+ try {
57
+ const pkg = JSON.parse(await readFile(resolvedPackageJson, "utf8"))
58
+ const actualVersion = pkg.version
59
+ if (actualVersion !== expectedVersion) {
60
+ mismatched.push({ name, expectedVersion, actualVersion })
61
+ }
62
+ } catch (error) {
63
+ mismatched.push({ name, expectedVersion, actualVersion: "unknown", reason: error.message })
64
+ }
65
+ }
66
+
67
+ return {
68
+ peers,
69
+ missing,
70
+ mismatched,
71
+ }
72
+ }
73
+
74
+ export function formatAuditMessage({ missing, mismatched }) {
75
+ if (!missing.length && !mismatched.length) {
76
+ return null
77
+ }
78
+
79
+ const lines = ["[signmax-config] Peer dependency check detected issues:"]
80
+
81
+ if (missing.length) {
82
+ lines.push(" Missing peers:")
83
+ for (const item of missing) {
84
+ lines.push(` - ${item.name}@${item.expectedVersion}`)
85
+ }
86
+ }
87
+
88
+ if (mismatched.length) {
89
+ lines.push(" Version mismatches:")
90
+ for (const item of mismatched) {
91
+ const details = item.actualVersion ? ` (found ${item.actualVersion})` : ""
92
+ lines.push(` - ${item.name}@${item.expectedVersion}${details}`)
93
+ }
94
+ }
95
+
96
+ lines.push(' Run "npx signmax-config-peers" to install the correct versions.')
97
+
98
+ return lines.join("\n")
99
+ }
100
+
101
+ export async function runPostinstallAudit() {
102
+ try {
103
+ const result = await auditPeerDependencies()
104
+ const message = formatAuditMessage(result)
105
+
106
+ if (message) {
107
+ console.warn(message)
108
+ }
109
+ } catch (error) {
110
+ console.warn(`[signmax-config] Peer dependency check skipped: ${error.message}`)
111
+ }
112
+ }
113
+
114
+ if (process.argv[1] === __filename) {
115
+ runPostinstallAudit()
116
+ }
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process"
3
+ import { existsSync } from "node:fs"
4
+ import { readFile } from "node:fs/promises"
5
+ import { dirname, resolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const HELP = `Usage: signmax-config-peers [options]
9
+
10
+ Options:
11
+ --manager <name> npm | pnpm | bun (auto-detected by default)
12
+ --dry-run Print the install command without running it
13
+ --help Show this help message
14
+ `
15
+
16
+ const __filename = fileURLToPath(import.meta.url)
17
+ const __dirname = dirname(__filename)
18
+ const defaultManifestPath = resolve(__dirname, "..", "..", "package.json")
19
+
20
+ export async function loadPeerDependencies(manifestPath = defaultManifestPath) {
21
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"))
22
+ const peers = manifest.peerDependencies ?? {}
23
+ return Object.entries(peers).map(([name, version]) => `${name}@${version}`)
24
+ }
25
+
26
+ export function detectManager({ managerArg, userAgent = "", cwd = process.cwd() } = {}) {
27
+ if (managerArg) return managerArg
28
+
29
+ if (userAgent.startsWith("pnpm/")) return "pnpm"
30
+ if (userAgent.startsWith("bun/")) return "bun"
31
+ if (userAgent.startsWith("npm/")) return "npm"
32
+
33
+ if (existsSync(resolve(cwd, "pnpm-lock.yaml"))) return "pnpm"
34
+ if (existsSync(resolve(cwd, "bun.lockb"))) return "bun"
35
+ if (existsSync(resolve(cwd, "package-lock.json"))) return "npm"
36
+
37
+ return "npm"
38
+ }
39
+
40
+ export function buildInstallCommand(manager, packages) {
41
+ if (!packages.length) {
42
+ throw new Error("No peer dependencies to install.")
43
+ }
44
+
45
+ const commandByManager = {
46
+ npm: {
47
+ command: "npm",
48
+ args: ["install", "--save-dev", "--save-exact", ...packages],
49
+ },
50
+ pnpm: {
51
+ command: "pnpm",
52
+ args: ["add", "-D", "--save-exact", ...packages],
53
+ },
54
+ bun: {
55
+ command: "bun",
56
+ args: ["add", "--dev", "--exact", ...packages],
57
+ },
58
+ }
59
+
60
+ const selected = commandByManager[manager]
61
+
62
+ if (!selected) {
63
+ const supported = Object.keys(commandByManager).join(", ")
64
+ throw new Error(`Unsupported package manager: ${manager}. Supported managers: ${supported}`)
65
+ }
66
+
67
+ return selected
68
+ }
69
+
70
+ export function formatCommand({ command, args }) {
71
+ return `${command} ${args.join(" ")}`
72
+ }
73
+
74
+ export async function prepareInstall({
75
+ managerArg,
76
+ cwd = process.cwd(),
77
+ env = process.env,
78
+ manifestPath = defaultManifestPath,
79
+ } = {}) {
80
+ const packages = await loadPeerDependencies(manifestPath)
81
+
82
+ if (packages.length === 0) {
83
+ return { packages: [] }
84
+ }
85
+
86
+ const manager = detectManager({ managerArg, userAgent: env.npm_config_user_agent ?? "", cwd })
87
+ const command = buildInstallCommand(manager, packages)
88
+
89
+ return {
90
+ manager,
91
+ packages,
92
+ command: command.command,
93
+ args: command.args,
94
+ printable: formatCommand(command),
95
+ }
96
+ }
97
+
98
+ export async function runCli({ args = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
99
+ if (args.includes("--help")) {
100
+ process.stdout.write(HELP)
101
+ return 0
102
+ }
103
+
104
+ const argValue = flag => {
105
+ const index = args.indexOf(flag)
106
+ if (index === -1) return undefined
107
+ return args[index + 1]
108
+ }
109
+
110
+ const managerArg = argValue("--manager")
111
+ const dryRun = args.includes("--dry-run")
112
+
113
+ try {
114
+ const result = await prepareInstall({ managerArg, cwd, env })
115
+
116
+ if (!result.packages || result.packages.length === 0) {
117
+ process.stdout.write("No peer dependencies found on @signmax/config.\n")
118
+ return 0
119
+ }
120
+
121
+ if (dryRun) {
122
+ process.stdout.write(`${result.printable}\n`)
123
+ return 0
124
+ }
125
+
126
+ process.stdout.write(`Running: ${result.printable}\n`)
127
+
128
+ const child = spawn(result.command, result.args, {
129
+ stdio: "inherit",
130
+ })
131
+
132
+ return await new Promise((resolve, reject) => {
133
+ child.on("close", code => {
134
+ if (code === 0) {
135
+ resolve(0)
136
+ } else {
137
+ reject(new Error(`${result.command} exited with code ${code}`))
138
+ }
139
+ })
140
+
141
+ child.on("error", error => {
142
+ reject(error)
143
+ })
144
+ })
145
+ } catch (error) {
146
+ process.stderr.write(`${error.message}\n`)
147
+ return 1
148
+ }
149
+ }
150
+
151
+ if (process.argv[1] === __filename) {
152
+ runCli()
153
+ .then(code => {
154
+ process.exit(code)
155
+ })
156
+ .catch(error => {
157
+ process.stderr.write(`${error.message}\n`)
158
+ process.exit(1)
159
+ })
160
+ }
@@ -0,0 +1,76 @@
1
+ import { mkdtemp, writeFile, mkdir } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { describe, expect, test } from "vitest"
6
+
7
+ import { auditPeerDependencies, formatAuditMessage } from "../scripts/peer-deps/audit.js"
8
+
9
+ const fixturePath = relative => fileURLToPath(new URL(`./fixtures/${relative}`, import.meta.url))
10
+
11
+ async function createPackage(root, name, version) {
12
+ const parts = name.split("/")
13
+ const packageDir = join(root, "node_modules", ...parts)
14
+ await mkdir(packageDir, { recursive: true })
15
+ await writeFile(join(packageDir, "package.json"), JSON.stringify({ name, version }), "utf8")
16
+ }
17
+
18
+ async function createProjectRoot() {
19
+ const root = await mkdtemp(join(tmpdir(), "signmax-check-peers-"))
20
+ await writeFile(join(root, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" }), "utf8")
21
+ return root
22
+ }
23
+
24
+ describe("check-peers audit", () => {
25
+ test("returns empty when all peers match", async () => {
26
+ const root = await createProjectRoot()
27
+ await createPackage(root, "eslint", "9.39.1")
28
+ await createPackage(root, "prettier", "3.6.2")
29
+
30
+ const result = await auditPeerDependencies({
31
+ manifestPath: fixturePath("manifest-with-peers.json"),
32
+ consumerRoot: root,
33
+ })
34
+
35
+ expect(result.missing).toEqual([])
36
+ expect(result.mismatched).toEqual([])
37
+ expect(formatAuditMessage(result)).toBeNull()
38
+ })
39
+
40
+ test("detects missing peers", async () => {
41
+ const root = await createProjectRoot()
42
+ await createPackage(root, "eslint", "9.39.1")
43
+
44
+ const result = await auditPeerDependencies({
45
+ manifestPath: fixturePath("manifest-with-peers.json"),
46
+ consumerRoot: root,
47
+ })
48
+
49
+ expect(result.missing.map(item => item.name)).toEqual(["prettier"])
50
+ const message = formatAuditMessage(result)
51
+ expect(message).toContain("Missing peers")
52
+ expect(message).toContain("prettier@3.6.2")
53
+ })
54
+
55
+ test("detects version mismatches", async () => {
56
+ const root = await createProjectRoot()
57
+ await createPackage(root, "eslint", "9.0.0")
58
+ await createPackage(root, "prettier", "3.6.2")
59
+
60
+ const result = await auditPeerDependencies({
61
+ manifestPath: fixturePath("manifest-with-peers.json"),
62
+ consumerRoot: root,
63
+ })
64
+
65
+ expect(result.mismatched).toEqual([
66
+ {
67
+ name: "eslint",
68
+ expectedVersion: "9.39.1",
69
+ actualVersion: "9.0.0",
70
+ },
71
+ ])
72
+ const message = formatAuditMessage(result)
73
+ expect(message).toContain("Version mismatches")
74
+ expect(message).toContain("eslint@9.39.1 (found 9.0.0)")
75
+ })
76
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "example"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "peerDependencies": {
3
+ "eslint": "9.39.1",
4
+ "prettier": "3.6.2"
5
+ }
6
+ }
@@ -0,0 +1,79 @@
1
+ import { mkdtemp, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { describe, expect, test } from "vitest"
6
+
7
+ import {
8
+ buildInstallCommand,
9
+ detectManager,
10
+ formatCommand,
11
+ loadPeerDependencies,
12
+ prepareInstall,
13
+ } from "../scripts/peer-deps/install.js"
14
+
15
+ const fixturePath = relative => fileURLToPath(new URL(`./fixtures/${relative}`, import.meta.url))
16
+
17
+ describe("install-peers helper", () => {
18
+ test("loadPeerDependencies returns pinned peers", async () => {
19
+ const peers = await loadPeerDependencies(fixturePath("manifest-with-peers.json"))
20
+ expect(peers).toEqual(["eslint@9.39.1", "prettier@3.6.2"])
21
+ })
22
+
23
+ test("loadPeerDependencies handles missing peers", async () => {
24
+ const peers = await loadPeerDependencies(fixturePath("manifest-no-peers.json"))
25
+ expect(peers).toEqual([])
26
+ })
27
+
28
+ test("detectManager respects explicit flag", () => {
29
+ const manager = detectManager({ managerArg: "pnpm" })
30
+ expect(manager).toBe("pnpm")
31
+ })
32
+
33
+ test("detectManager infers from user agent", () => {
34
+ const manager = detectManager({ userAgent: "pnpm/9.0.0 npm/?" })
35
+ expect(manager).toBe("pnpm")
36
+ })
37
+
38
+ test("detectManager infers from lockfile", async () => {
39
+ const dir = await mkdtemp(join(tmpdir(), "signmax-config-"))
40
+ await writeFile(join(dir, "bun.lockb"), "")
41
+ const manager = detectManager({ cwd: dir })
42
+ expect(manager).toBe("bun")
43
+ })
44
+
45
+ test("buildInstallCommand supports npm", () => {
46
+ const result = buildInstallCommand("npm", ["eslint@9.39.1"])
47
+ expect(result.command).toBe("npm")
48
+ expect(result.args).toEqual(["install", "--save-dev", "--save-exact", "eslint@9.39.1"])
49
+ expect(formatCommand(result)).toBe("npm install --save-dev --save-exact eslint@9.39.1")
50
+ })
51
+
52
+ test("buildInstallCommand throws on unsupported manager", () => {
53
+ expect(() => buildInstallCommand("yarn", ["eslint@9.39.1"])).toThrow(/Unsupported package manager/)
54
+ })
55
+
56
+ test("prepareInstall returns printable command", async () => {
57
+ const result = await prepareInstall({
58
+ managerArg: "npm",
59
+ env: {},
60
+ cwd: process.cwd(),
61
+ manifestPath: fixturePath("manifest-with-peers.json"),
62
+ })
63
+
64
+ expect(result.manager).toBe("npm")
65
+ expect(result.packages).toEqual(["eslint@9.39.1", "prettier@3.6.2"])
66
+ expect(result.printable).toBe("npm install --save-dev --save-exact eslint@9.39.1 prettier@3.6.2")
67
+ })
68
+
69
+ test("prepareInstall returns empty when no peers", async () => {
70
+ const result = await prepareInstall({
71
+ managerArg: "npm",
72
+ env: {},
73
+ cwd: process.cwd(),
74
+ manifestPath: fixturePath("manifest-no-peers.json"),
75
+ })
76
+
77
+ expect(result.packages).toEqual([])
78
+ })
79
+ })
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skyltmax_config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Signmax AB
@@ -112,7 +112,13 @@ files:
112
112
  - reset.d.ts
113
113
  - rubocop.rails.yml
114
114
  - rubocop.yml
115
+ - scripts/peer-deps/audit.js
116
+ - scripts/peer-deps/install.js
115
117
  - skyltmax_config.gemspec
118
+ - tests/check-peers.test.js
119
+ - tests/fixtures/manifest-no-peers.json
120
+ - tests/fixtures/manifest-with-peers.json
121
+ - tests/install-peers.test.js
116
122
  - tsconfig.json
117
123
  - typescript.json
118
124
  homepage: https://github.com/skyltmax/config