@ditojs/build 2.89.0 → 2.90.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/build",
3
- "version": "2.89.0",
3
+ "version": "2.90.0",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/ditojs/dito/tree/main/packages/build",
6
6
  "author": "Jürg Lehni <juerg@scratchdisk.com> (http://scratchdisk.com)",
@@ -13,7 +13,9 @@
13
13
  "src/"
14
14
  ],
15
15
  "bin": {
16
- "inline-assets": "./src/cli/inline-assets.js"
16
+ "inline-assets": "./src/cli/inline-assets.js",
17
+ "pnpm-link": "./src/cli/pnpm-link.js",
18
+ "pnpm-unlink": "./src/cli/pnpm-unlink.js"
17
19
  },
18
20
  "engines": {
19
21
  "node": ">= 20.0.0"
@@ -22,7 +24,7 @@
22
24
  "node >= 18"
23
25
  ],
24
26
  "dependencies": {
25
- "@ditojs/utils": "^2.89.0",
27
+ "@ditojs/utils": "^2.90.0",
26
28
  "@vitejs/plugin-vue": "^6.0.4",
27
29
  "autoprefixer": "^10.4.27",
28
30
  "mime": "^4.1.0",
@@ -37,5 +39,5 @@
37
39
  "vite": "^7.3.1",
38
40
  "vue": "^3.5.30"
39
41
  },
40
- "gitHead": "9978921be041dd74026e5c8cc4e889f4ff3303c7"
42
+ "gitHead": "4e5489c32920078f98f23460cef00d658216b878"
41
43
  }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { link } from '../pnpm-linking.js'
4
+
5
+ const names = process.argv.slice(2)
6
+ if (names.length === 0) {
7
+ console.error('Usage: pnpm-link <repo> ...')
8
+ process.exit(1)
9
+ }
10
+
11
+ link(names)
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'node:child_process'
4
+ import { link, listLinks, unlink } from '../pnpm-linking.js'
5
+
6
+ const names = process.argv.slice(2)
7
+ const links = listLinks()
8
+
9
+ // Determine which repos to unlink and which to keep.
10
+ const toUnlink =
11
+ names.length > 0
12
+ ? links.filter(link => names.includes(link))
13
+ : links
14
+ const toKeep = links.filter(link => !toUnlink.includes(link))
15
+
16
+ // Remove linked symlinks so pnpm install can restore them.
17
+ unlink(toUnlink)
18
+
19
+ // Restore node_modules from the unchanged lockfile.
20
+ execSync('pnpm install --frozen-lockfile', { stdio: 'inherit' })
21
+
22
+ // Re-link repos that weren't specified for unlinking.
23
+ if (toKeep.length > 0) {
24
+ link(toKeep)
25
+ }
@@ -0,0 +1,194 @@
1
+ /* eslint-disable no-console */
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ readlinkSync,
9
+ rmSync,
10
+ symlinkSync
11
+ } from 'node:fs'
12
+ import { relative, resolve } from 'node:path'
13
+
14
+ // Resolve workspace package directories for a sibling workspace. For
15
+ // monorepos, parses pnpm-workspace.yaml to find all packages. For
16
+ // single-package repos, returns the workspace root.
17
+ function getWorkspacePackages(name) {
18
+ const dir = resolve('..', name)
19
+ const workspaceFile = resolve(dir, 'pnpm-workspace.yaml')
20
+ if (!existsSync(workspaceFile)) {
21
+ return [{ dir, ...readPackageMeta(dir) }]
22
+ }
23
+ const content = readFileSync(workspaceFile, 'utf8')
24
+ const packages = []
25
+ for (const [, prefix] of content.matchAll(/- ['"]?(.+?)\/\*['"]?/g)) {
26
+ const pkgDir = resolve(dir, prefix)
27
+ for (const entry of readdirSync(pkgDir)) {
28
+ const pkgPath = resolve(pkgDir, entry)
29
+ if (existsSync(resolve(pkgPath, 'package.json'))) {
30
+ packages.push({ dir: pkgPath, ...readPackageMeta(pkgPath) })
31
+ }
32
+ }
33
+ }
34
+ return packages
35
+ }
36
+
37
+ function readPackageMeta(dir) {
38
+ const { name } = JSON.parse(
39
+ readFileSync(resolve(dir, 'package.json'), 'utf8')
40
+ )
41
+ const parts = name.split('/')
42
+ const scoped = parts.length === 2
43
+ return {
44
+ name,
45
+ // Subpath within node_modules (e.g. '@scope/pkg' -> ['@scope', 'pkg'])
46
+ moduleParts: scoped ? [parts[0], parts[1]] : [name],
47
+ // .pnpm directory prefix (e.g. '@scope/pkg' -> '@scope+pkg@')
48
+ pnpmPrefix: scoped ? `${parts[0]}+${parts[1]}@` : `${name}@`
49
+ }
50
+ }
51
+
52
+ // Yield [pkgDir, entry] pairs from .pnpm/ matching a package.
53
+ function* getPnpmEntries(pnpmDir, { moduleParts, pnpmPrefix }) {
54
+ if (existsSync(pnpmDir)) {
55
+ for (const entry of readdirSync(pnpmDir)) {
56
+ if (entry.startsWith(pnpmPrefix)) {
57
+ const dir = resolve(pnpmDir, entry, 'node_modules', ...moduleParts)
58
+ if (existsSync(dir)) {
59
+ yield [dir, entry]
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ function replaceWithSymlink(target, pkgDir) {
67
+ rmSync(target, { recursive: true, force: true })
68
+ symlinkSync(pkgDir, target)
69
+ }
70
+
71
+ export function link(names) {
72
+ const linkedNames = new Set()
73
+ const repoDirs = []
74
+ const nodeModules = resolve('node_modules')
75
+ const pnpmDir = resolve(nodeModules, '.pnpm')
76
+
77
+ for (const name of names) {
78
+ repoDirs.push(resolve('..', name))
79
+
80
+ for (const meta of getWorkspacePackages(name)) {
81
+ linkedNames.add(meta.name)
82
+ const { moduleParts } = meta
83
+
84
+ // Replace the top-level symlink with a direct link to the local
85
+ // workspace.
86
+ const target = resolve(nodeModules, ...moduleParts)
87
+ if (moduleParts.length > 1) {
88
+ mkdirSync(resolve(nodeModules, moduleParts[0]), { recursive: true })
89
+ }
90
+ replaceWithSymlink(target, meta.dir)
91
+ console.log(` linked ${meta.name} → ${meta.dir}`)
92
+
93
+ // Also replace real directories inside .pnpm/ so that other packages that
94
+ // depend on this one also resolve to the local workspace.
95
+ for (const [dir, entry] of getPnpmEntries(pnpmDir, meta)) {
96
+ if (!lstatSync(dir).isSymbolicLink()) {
97
+ replaceWithSymlink(dir, meta.dir)
98
+ console.log(` linked .pnpm/${entry} → ${meta.dir}`)
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // Symlink missing dependencies from linked repos into node_modules, so that
105
+ // bundlers with preserveSymlinks can resolve them from the symlink location.
106
+ function symlinkIfMissing(source, target, name) {
107
+ if (linkedNames.has(name)) return
108
+ // Use lstatSync to detect broken symlinks that existsSync would miss.
109
+ if (lstatSync(target, { throwIfNoEntry: false })) return
110
+ symlinkSync(source, target)
111
+ console.log(` symlinked ${name}`)
112
+ }
113
+
114
+ for (const repoDir of repoDirs) {
115
+ const repoModules = resolve(repoDir, 'node_modules')
116
+ if (!existsSync(repoModules)) continue
117
+ for (const entry of readdirSync(repoModules)) {
118
+ if (entry.startsWith('.')) continue
119
+ if (entry.startsWith('@')) {
120
+ const scopeDir = resolve(repoModules, entry)
121
+ const localScopeDir = resolve(nodeModules, entry)
122
+ for (const scopedEntry of readdirSync(scopeDir)) {
123
+ mkdirSync(localScopeDir, { recursive: true })
124
+ symlinkIfMissing(
125
+ resolve(scopeDir, scopedEntry),
126
+ resolve(localScopeDir, scopedEntry),
127
+ `${entry}/${scopedEntry}`
128
+ )
129
+ }
130
+ } else {
131
+ symlinkIfMissing(
132
+ resolve(repoModules, entry),
133
+ resolve(nodeModules, entry),
134
+ entry
135
+ )
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ export function unlink(names) {
142
+ const nodeModules = resolve('node_modules')
143
+ const pnpmDir = resolve(nodeModules, '.pnpm')
144
+
145
+ for (const name of names) {
146
+ for (const meta of getWorkspacePackages(name)) {
147
+ // Remove the top-level linked symlink.
148
+ rmSync(resolve(nodeModules, ...meta.moduleParts), { force: true })
149
+ console.log(` unlinked ${meta.name}`)
150
+
151
+ // Remove linked symlinks inside .pnpm/.
152
+ for (const [dir, entry] of getPnpmEntries(pnpmDir, meta)) {
153
+ if (lstatSync(dir).isSymbolicLink()) {
154
+ rmSync(dir, { force: true })
155
+ console.log(` unlinked .pnpm/${entry}`)
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ export function listLinks() {
163
+ const nodeModules = resolve('node_modules')
164
+ const parentDir = resolve('..')
165
+ const repos = new Set()
166
+
167
+ function checkEntry(target) {
168
+ const stat = lstatSync(target, { throwIfNoEntry: false })
169
+ if (!stat?.isSymbolicLink()) return
170
+ const linkTarget = readlinkSync(target)
171
+ const resolved = resolve(target, '..', linkTarget)
172
+ // A linked symlink points outside node_modules
173
+ if (!resolved.startsWith(nodeModules)) {
174
+ const rel = relative(parentDir, resolved)
175
+ if (!rel.startsWith('..')) {
176
+ repos.add(rel.split('/')[0])
177
+ }
178
+ }
179
+ }
180
+
181
+ for (const entry of readdirSync(nodeModules)) {
182
+ if (entry.startsWith('.')) continue
183
+ if (entry.startsWith('@')) {
184
+ const scopeDir = resolve(nodeModules, entry)
185
+ for (const scopedEntry of readdirSync(scopeDir)) {
186
+ checkEntry(resolve(scopeDir, scopedEntry))
187
+ }
188
+ } else {
189
+ checkEntry(resolve(nodeModules, entry))
190
+ }
191
+ }
192
+
193
+ return [...repos]
194
+ }