create-pkgbld 1.8.2 → 2.0.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.
package/README.md CHANGED
@@ -1,22 +1,97 @@
1
1
  # create-pkgbld
2
2
 
3
- Minimalistic scaffolding utility for [pkgbld](https://github.com/kshutkin/package-build/tree/main/pkgbld).
4
-
5
- *Disclaimer:* Despite the version being 1.0.0+ it is still very raw, use only if it fits your needs.
6
-
7
- [Changelog](./CHANGELOG.md)
3
+ Scaffold packages built with [PKG BLD](https://github.com/kshutkin/package-build/tree/main/pkgbld) and manage their build plugins and project setup extensions.
8
4
 
9
5
  ## Usage
10
6
 
11
- ```
7
+ ```sh
12
8
  npm init pkgbld
9
+ npm init pkgbld <directory>
13
10
  ```
14
11
 
15
- or
12
+ For a new package, `create-pkgbld` generates the initial package metadata and
13
+ README. In an existing package, it opens the interactive package manager for
14
+ adding, removing, or adopting integrations. Pending changes are shown before
15
+ they are written.
16
+
17
+ Use `--quiet` (`-q`) to suppress informational output and the interactive
18
+ package manager. Use `--install` to run the detected package manager after
19
+ dependency changes.
20
+
21
+ ## Package management
22
+
23
+ | Command | Behavior |
24
+ |---|---|
25
+ | `create-pkgbld list` | List official integrations, locked integrations, and modern plugins declared by the project. Listing is offline and does not change the project. |
26
+ | `create-pkgbld add <package>` | Apply an official integration, restore a locked one, or adopt an installed unmanaged plugin. |
27
+ | `create-pkgbld remove <package>` | Remove an integration and its project-lock entry. Unregistered plugins are removed from every dependency field. |
28
+ | `create-pkgbld update <package>` | Update one managed official integration to the newest stable version allowed by its registry range. |
29
+
30
+ Package names may be registry names such as `biome` and
31
+ `pkgbld-dts-buddy`, or the full name of a discovered plugin.
32
+
33
+ ### Flags
34
+
35
+ | Flag | Description |
36
+ |---|---|
37
+ | `--yes`, `-y` | Use extension prompt defaults. It does not approve update conflicts. |
38
+ | `--dry-run` | Resolve and display changes without writing the project or installing dependencies. |
39
+ | `--quiet`, `-q` | Suppress informational output. |
40
+ | `--install` | Run the detected package manager after add, remove, or extension-update dependency changes. |
41
+ | `--accept-conflicts` | Update only: accept proposed replacements for resources customized since the locked version. |
16
42
 
43
+ Build plugin updates always install and verify the exact target version before
44
+ advancing the project lock, regardless of `--install`.
45
+
46
+ ```sh
47
+ create-pkgbld add biome --yes --dry-run
48
+ create-pkgbld add biome --yes --install
49
+ create-pkgbld remove biome --yes
50
+ create-pkgbld update biome --dry-run
51
+ create-pkgbld update biome --yes --accept-conflicts
17
52
  ```
18
- npm init pkgbld <folder name>
53
+
54
+ ## Official integrations
55
+
56
+ | Name | Purpose |
57
+ |---|---|
58
+ | `biome` | Biome linting and formatting |
59
+ | `pkgbld-swc` | TypeScript stripping through `pkgbld-plugin-swc` |
60
+ | `dts-buddy` | Standalone declaration bundling |
61
+ | `pkgbld-dts-buddy` | Declaration bundling through `pkgbld-plugin-dts-buddy` |
62
+
63
+ The bundled registry contains metadata only. Selected extension versions are
64
+ downloaded into a version-isolated shared cache located with
65
+ `find-cache-directory`; set `CREATE_PKGBLD_CACHE_DIR` to override its location.
66
+ Extension packages are not added to the target project. Build plugins and tools
67
+ configured by an extension are normal project dependencies.
68
+
69
+ Applied or adopted integrations are recorded by canonical package name and
70
+ exact version in the committed `.pkgbld-lock.json`:
71
+
72
+ ```json
73
+ {
74
+ "$schema": "https://unpkg.com/create-pkgbld/lock-schema-v1.json",
75
+ "packages": {
76
+ "create-pkgbld-extension-biome": "0.1.1",
77
+ "@author/pkgbld-plugin-example": "2.1.0"
78
+ }
79
+ }
19
80
  ```
20
- # License
21
81
 
22
- [MIT](https://github.com/kshutkin/package-build/blob/main/LICENSE)
82
+ Plugins named `pkgbld-plugin-*` or `@scope/pkgbld-plugin-*` are discovered in
83
+ `dependencies`, `devDependencies`, and `peerDependencies` without executing
84
+ their code. A manageable plugin must declare a `pkgbld` range in
85
+ `peerDependencies`. The range is validated before add and update; legacy or
86
+ unresolved plugins are excluded with an actionable warning.
87
+
88
+ Updates compare the locked extension contract, the current project, and the
89
+ target contract. Unchanged resources update automatically. Customized
90
+ resources are displayed as conflicts and require interactive approval or
91
+ `--accept-conflicts`.
92
+
93
+ See the [PKG BLD plugin interface](https://github.com/kshutkin/package-build/blob/main/pkgbld/README.md#build-plugin-interface) for build plugin authoring,
94
+ [EXTENSIONS.md](./EXTENSIONS.md) for optional project setup behavior, and
95
+ [DESIGN.md](./DESIGN.md) for package-management semantics.
96
+
97
+ [Changelog](./CHANGELOG.md) · [License](https://github.com/kshutkin/package-build/blob/main/LICENSE)
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "create-pkgbld extension registry",
4
+ "description": "Registry file describing extensions available to create-pkgbld.",
5
+ "type": "object",
6
+ "required": ["extensions"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "Optional schema reference."
12
+ },
13
+ "extensions": {
14
+ "type": "array",
15
+ "description": "List of extensions exposed by this registry.",
16
+ "items": { "$ref": "#/definitions/extensionEntry" }
17
+ }
18
+ },
19
+ "definitions": {
20
+ "extensionEntry": {
21
+ "type": "object",
22
+ "required": ["name", "package", "description"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "name": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "description": "Unique extension name as referenced from CLI (e.g. `create-pkgbld add <name>`)."
29
+ },
30
+ "package": {
31
+ "type": "string",
32
+ "minLength": 1,
33
+ "pattern": "^(?:@[a-z0-9][a-z0-9._~-]*/)?(?:create-pkgbld-extension-|pkgbld-plugin-)[a-z0-9][a-z0-9._~-]*(?:/extension)?$",
34
+ "description": "Canonical create-pkgbld extension package or PKG BLD plugin, optionally with /extension."
35
+ },
36
+ "version": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "description": "Version range installed in the shared extension cache. Defaults to latest."
40
+ },
41
+ "description": {
42
+ "type": "string",
43
+ "description": "Short human description shown in the list / TUI."
44
+ },
45
+ "tags": {
46
+ "type": "array",
47
+ "description": "Optional tags for grouping/filtering.",
48
+ "items": { "type": "string", "minLength": 1 }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "./extensions-schema.json",
3
+ "extensions": [
4
+ {
5
+ "name": "biome",
6
+ "package": "create-pkgbld-extension-biome",
7
+ "version": "^0.1.0",
8
+ "description": "Biome linter and formatter",
9
+ "tags": ["linter", "formatter"]
10
+ },
11
+ {
12
+ "name": "pkgbld-swc",
13
+ "package": "pkgbld-plugin-swc/extension",
14
+ "version": "^1.0.0",
15
+ "description": "SWC TypeScript stripping via pkgbld",
16
+ "tags": ["plugin", "typescript", "pkgbld"]
17
+ },
18
+ {
19
+ "name": "pkgbld-dts-buddy",
20
+ "package": "pkgbld-plugin-dts-buddy/extension",
21
+ "version": "^1.0.0",
22
+ "description": "Generate d.ts files using dts-buddy",
23
+ "tags": ["plugin", "types", "pkgbld"]
24
+ },
25
+ {
26
+ "name": "dts-buddy",
27
+ "package": "create-pkgbld-extension-dts-buddy",
28
+ "version": "^0.1.0",
29
+ "description": "Standalone d.ts bundling using dts-buddy",
30
+ "tags": ["types"]
31
+ }
32
+ ]
33
+ }
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
- import('./dist/index.mjs');
2
+
3
+ import('./src/index.js');
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/create-pkgbld/lock-schema-v1.json",
4
+ "title": "create-pkgbld lock file, version 1",
5
+ "description": "Records PKG BLD integrations acknowledged as applied to a project.",
6
+ "type": "object",
7
+ "required": ["$schema", "packages"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "const": "https://unpkg.com/create-pkgbld/lock-schema-v1.json"
12
+ },
13
+ "packages": {
14
+ "type": "object",
15
+ "description": "Canonical npm package names mapped to exact applied versions.",
16
+ "propertyNames": {
17
+ "type": "string",
18
+ "pattern": "^(?:@[a-z0-9][a-z0-9._~-]*/)?(?:create-pkgbld-extension-|pkgbld-plugin-)[a-z0-9][a-z0-9._~-]*$"
19
+ },
20
+ "additionalProperties": {
21
+ "type": "string",
22
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
23
+ }
24
+ }
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
- "version": "1.8.2",
2
+ "version": "2.0.1",
3
3
  "name": "create-pkgbld",
4
4
  "license": "MIT",
5
5
  "author": "Konstantin Shutkin",
6
6
  "bin": "./index.js",
7
- "main": "./dist/index.mjs",
7
+ "main": "./src/index.js",
8
8
  "type": "module",
9
+ "files": [
10
+ "src",
11
+ "index.js",
12
+ "extensions.json",
13
+ "extensions-schema.json",
14
+ "lock-schema-v1.json"
15
+ ],
9
16
  "engines": {
10
17
  "node": ">=20"
11
18
  },
@@ -23,12 +30,20 @@
23
30
  "init"
24
31
  ],
25
32
  "dependencies": {
26
- "string-argv": "0.3.2",
27
- "git-user-name": "2.0.0",
28
- "cleye": "1.3.4",
29
- "parse-git-config": "3.0.0",
33
+ "@niceties/ansi": "^1.1.2",
34
+ "@niceties/node-parseargs-plus": "^0.6.0",
35
+ "find-cache-directory": "^6.0.0",
30
36
  "prompts": "2.4.2",
31
- "kleur": "4.1.5",
32
- "lodash": "4.17.21"
37
+ "semver": "7.8.5",
38
+ "pkgbld": "2.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/prompts": "2.4.9",
42
+ "@types/semver": "7.8.0",
43
+ "dts-buddy": "^0.8.3"
44
+ },
45
+ "scripts": {
46
+ "build": "echo 'no build needed'",
47
+ "test": "node --test 'test/*.test.js'"
33
48
  }
34
49
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Format conflicts for printing. Returns lines with a leading marker.
3
+ * @param {readonly import('./project-changes.js').Conflict[]} conflicts
4
+ * @returns {string[]}
5
+ */
6
+ export function formatConflicts(conflicts) {
7
+ return conflicts.map(conflict => {
8
+ const source = `(from: ${[...new Set(conflict.sources)].join(', ')})`;
9
+ if (conflict.kind !== 'migration-conflict') return ` ⚠ ${conflict.message} ${source}`;
10
+ return ` ⚠ ${conflict.message}; expected ${formatValue(conflict.expected)}, found ${formatValue(
11
+ conflict.current
12
+ )}, proposed ${formatValue(conflict.proposed)} ${source}`;
13
+ });
14
+ }
15
+
16
+ /** @param {unknown} value */
17
+ function formatValue(value) {
18
+ if (value === undefined) return '<absent>';
19
+ const json = JSON.stringify(value);
20
+ return json && json.length > 120 ? `${json.slice(0, 117)}...` : (json ?? String(value));
21
+ }
package/src/diff.js ADDED
@@ -0,0 +1,83 @@
1
+ import { blue, gray, green, red, white, yellow } from '@niceties/ansi';
2
+
3
+ /**
4
+ * @typedef {import('./tree.js').FileChange} FileChange
5
+ */
6
+
7
+ /**
8
+ * Render an accumulated set of FileChanges as a colored, file-by-file diff
9
+ * suitable for printing before commit (or for --dry-run output).
10
+ *
11
+ * Pure: returns a string with no trailing newline; the caller decides
12
+ * whether to wrap with headers or pipe through console.log.
13
+ *
14
+ * @param {readonly FileChange[]} changes
15
+ * @param {{ projectRoot?: string, readDiskJson?: (path: string) => any | null }} [opts]
16
+ * @returns {string}
17
+ */
18
+ export function renderChanges(changes, opts = {}) {
19
+ if (changes.length === 0) return gray(' (no changes)');
20
+ const lines = [];
21
+ for (const c of changes) {
22
+ const tag = c.type === 'CREATE' ? green('CREATE') : c.type === 'UPDATE' ? yellow('UPDATE') : red('DELETE');
23
+ lines.push(` ${tag} ${white(c.path)}`);
24
+ if (c.type === 'UPDATE' && c.path.endsWith('package.json') && typeof c.content === 'string') {
25
+ const before = opts.readDiskJson?.(c.path) ?? null;
26
+ const after = safeParseJson(c.content);
27
+ if (before && after) {
28
+ const keyLines = describePackageJsonDiff(before, after);
29
+ for (const kl of keyLines) lines.push(` ${kl}`);
30
+ }
31
+ } else if (c.type === 'DELETE') {
32
+ lines.push(` ${gray('(file removed)')}`);
33
+ }
34
+ }
35
+ return lines.join('\n');
36
+ }
37
+
38
+ /**
39
+ * @param {string} text
40
+ */
41
+ function safeParseJson(text) {
42
+ try {
43
+ return JSON.parse(text);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ const PKG_GROUPS = ['dependencies', 'devDependencies', 'peerDependencies', 'scripts'];
50
+
51
+ /**
52
+ * @param {Record<string, any>} before
53
+ * @param {Record<string, any>} after
54
+ * @returns {string[]}
55
+ */
56
+ function describePackageJsonDiff(before, after) {
57
+ /** @type {string[]} */
58
+ const out = [];
59
+ for (const group of PKG_GROUPS) {
60
+ const b = before[group] ?? {};
61
+ const a = after[group] ?? {};
62
+ const keys = new Set([...Object.keys(b), ...Object.keys(a)]);
63
+ for (const k of keys) {
64
+ if (b[k] === a[k]) continue;
65
+ if (!(k in b)) {
66
+ out.push(`${green('+')} ${gray(group)}.${white(k)} ${gray('=')} ${green(String(a[k]))}`);
67
+ } else if (!(k in a)) {
68
+ out.push(`${red('-')} ${gray(group)}.${white(k)}`);
69
+ } else {
70
+ out.push(`${blue('~')} ${gray(group)}.${white(k)} ${gray(String(b[k]))} ${gray('→')} ${white(String(a[k]))}`);
71
+ }
72
+ }
73
+ }
74
+ const topKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
75
+ for (const k of topKeys) {
76
+ if (PKG_GROUPS.includes(k)) continue;
77
+ if (JSON.stringify(before[k]) === JSON.stringify(after[k])) continue;
78
+ if (!(k in before)) out.push(`${green('+')} ${white(k)}`);
79
+ else if (!(k in after)) out.push(`${red('-')} ${white(k)}`);
80
+ else out.push(`${blue('~')} ${white(k)}`);
81
+ }
82
+ return out;
83
+ }
package/src/engine.js ADDED
@@ -0,0 +1,120 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /**
4
+ * @typedef {import('./tree.js').Tree} Tree
5
+ * @typedef {import('./registry.js').Extension} Extension
6
+ * @typedef {import('./registry.js').SetupDeclarative} SetupDeclarative
7
+ * @typedef {import('./registry.js').RemoveDeclarative} RemoveDeclarative
8
+ * @typedef {import('./types.js').OptionsValue} OptionsValue
9
+ */
10
+
11
+ /**
12
+ * Run an extension's setup against the given tree.
13
+ *
14
+ * @param {Extension} extension
15
+ * @param {Tree} tree
16
+ * @param {OptionsValue} [options]
17
+ */
18
+ export async function runSetup(extension, tree, options = {}) {
19
+ if (!extension.setup) return;
20
+ tree.setExtensionBase(extension.__baseDir ?? null);
21
+ try {
22
+ if (typeof extension.setup === 'function') {
23
+ await extension.setup(tree, options);
24
+ } else {
25
+ applyDeclarativeSetup(extension.setup, tree);
26
+ }
27
+ } finally {
28
+ tree.setExtensionBase(null);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Run an extension's remove against the given tree.
34
+ *
35
+ * @param {Extension} extension
36
+ * @param {Tree} tree
37
+ * @param {OptionsValue} [options]
38
+ */
39
+ export async function runRemove(extension, tree, options = {}) {
40
+ if (!extension.remove) return;
41
+ tree.setExtensionBase(extension.__baseDir ?? null);
42
+ try {
43
+ if (typeof extension.remove === 'function') {
44
+ await extension.remove(tree, options);
45
+ } else {
46
+ applyDeclarativeRemove(extension.remove, tree);
47
+ }
48
+ } finally {
49
+ tree.setExtensionBase(null);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * @param {Extension} extension
55
+ * @param {Tree} tree
56
+ */
57
+ export function detectExtension(extension, tree) {
58
+ if (typeof extension.detect !== 'function') return false;
59
+ return Boolean(extension.detect(tree));
60
+ }
61
+
62
+ /**
63
+ * @param {SetupDeclarative} setup
64
+ * @param {Tree} tree
65
+ */
66
+ function applyDeclarativeSetup(setup, tree) {
67
+ if (setup.dependencies) {
68
+ for (const [name, version] of Object.entries(setup.dependencies)) {
69
+ tree.addDependency(name, version, 'dependencies');
70
+ }
71
+ }
72
+ if (setup.devDependencies) {
73
+ for (const [name, version] of Object.entries(setup.devDependencies)) {
74
+ tree.addDependency(name, version, 'devDependencies');
75
+ }
76
+ }
77
+ if (setup.scripts) {
78
+ for (const [name, command] of Object.entries(setup.scripts)) {
79
+ tree.addScript(name, command);
80
+ }
81
+ }
82
+ if (setup.files) {
83
+ for (const [target, source] of Object.entries(setup.files)) {
84
+ if (source.startsWith('inline:')) {
85
+ tree.write(target, source.slice('inline:'.length));
86
+ } else {
87
+ const abs = tree.resolveExtensionFile(source);
88
+ tree.write(target, readFileSync(abs, 'utf8'));
89
+ }
90
+ }
91
+ }
92
+ if (setup.packageJson) {
93
+ const extra = setup.packageJson;
94
+ tree.updateJson('package.json', pkg => {
95
+ for (const [key, value] of Object.entries(extra)) {
96
+ pkg[key] = value;
97
+ }
98
+ return pkg;
99
+ });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {RemoveDeclarative} remove
105
+ * @param {Tree} tree
106
+ */
107
+ function applyDeclarativeRemove(remove, tree) {
108
+ if (remove.dependencies) {
109
+ for (const name of remove.dependencies) tree.removeDependency(name, 'dependencies');
110
+ }
111
+ if (remove.devDependencies) {
112
+ for (const name of remove.devDependencies) tree.removeDependency(name, 'devDependencies');
113
+ }
114
+ if (remove.scripts) {
115
+ for (const name of remove.scripts) tree.removeScript(name);
116
+ }
117
+ if (remove.files) {
118
+ for (const file of remove.files) tree.delete(file);
119
+ }
120
+ }
@@ -0,0 +1,110 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import findCacheDirectory from 'find-cache-directory';
6
+ import semver from 'semver';
7
+
8
+ import { resolvePublishedVersion } from './package-version.js';
9
+
10
+ /** @returns {string} */
11
+ export function getExtensionCacheDir() {
12
+ if (process.env.CREATE_PKGBLD_CACHE_DIR) return path.resolve(process.env.CREATE_PKGBLD_CACHE_DIR);
13
+ const cacheDir = findCacheDirectory({ name: 'create-pkgbld', cwd: import.meta.dirname });
14
+ if (!cacheDir) throw new Error('Cannot locate a shared cache directory for create-pkgbld');
15
+ return path.join(cacheDir, 'extensions');
16
+ }
17
+
18
+ /**
19
+ * Return the installable package name from a package or package/subpath specifier.
20
+ * Relative and absolute paths are intentionally not installable.
21
+ * @param {string} specifier
22
+ */
23
+ export function getPackageName(specifier) {
24
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')) return null;
25
+ const parts = specifier.split('/');
26
+ if (specifier.startsWith('@')) return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
27
+ return parts[0] || null;
28
+ }
29
+
30
+ /** @param {string} packageName @param {string} version @param {string} [cacheRoot] */
31
+ export function getExtensionCacheSlot(packageName, version, cacheRoot = getExtensionCacheDir()) {
32
+ return path.join(cacheRoot, encodeURIComponent(packageName), version);
33
+ }
34
+
35
+ /**
36
+ * Return cached exact-version roots newest first.
37
+ * @param {string} packageName
38
+ * @param {string | undefined} selector
39
+ * @param {string} [cacheRoot]
40
+ */
41
+ export async function listCachedExtensionSlots(packageName, selector, cacheRoot = getExtensionCacheDir()) {
42
+ const packageRoot = path.join(cacheRoot, encodeURIComponent(packageName));
43
+ let versions;
44
+ try {
45
+ versions = await readdir(packageRoot);
46
+ } catch {
47
+ return [];
48
+ }
49
+ return semver
50
+ .rsort(versions.filter(version => semver.valid(version) && (!selector || semver.satisfies(version, selector))))
51
+ .map(version => ({ version, cacheDir: getExtensionCacheSlot(packageName, version, cacheRoot) }));
52
+ }
53
+
54
+ /**
55
+ * Install one exact extension version in an immutable shared-cache slot.
56
+ * A range is resolved before the slot is selected.
57
+ * @param {{ package: string, version?: string }} entry
58
+ * @param {string} [cacheRoot]
59
+ * @param {(command: string, args: string[], cwd: string) => Promise<number>} [runner]
60
+ * @param {(packageName: string, selector: string) => Promise<string>} [versionResolver]
61
+ */
62
+ export async function installCachedExtension(
63
+ entry,
64
+ cacheRoot = getExtensionCacheDir(),
65
+ runner = runCommand,
66
+ versionResolver = resolvePublishedVersion
67
+ ) {
68
+ const packageName = getPackageName(entry.package);
69
+ if (!packageName) throw new Error(`Extension package "${entry.package}" cannot be installed in the shared cache`);
70
+
71
+ const selector = entry.version ?? 'latest';
72
+ const exactVersion = semver.valid(selector) ?? (await versionResolver(packageName, selector));
73
+ const cacheDir = getExtensionCacheSlot(packageName, exactVersion, cacheRoot);
74
+ const manifestPath = path.join(cacheDir, 'package.json');
75
+ const packageManifest = path.join(cacheDir, 'node_modules', packageName, 'package.json');
76
+ if (await hasCachedPackage(packageManifest, exactVersion)) return { cacheDir, version: exactVersion };
77
+
78
+ await mkdir(cacheDir, { recursive: true });
79
+ const manifest = { private: true, dependencies: { [packageName]: exactVersion } };
80
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
81
+
82
+ const code = await runner('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], cacheDir);
83
+ if (code !== 0) {
84
+ await writeFile(manifestPath, `${JSON.stringify({ private: true, dependencies: {} }, null, 2)}\n`);
85
+ throw new Error(`Failed to cache extension package "${packageName}@${exactVersion}" (npm install exited with code ${code})`);
86
+ }
87
+ if (!(await hasCachedPackage(packageManifest, exactVersion))) {
88
+ throw new Error(`npm install completed without caching extension package "${packageName}@${exactVersion}"`);
89
+ }
90
+ return { cacheDir, version: exactVersion };
91
+ }
92
+
93
+ /** @param {string} manifestPath @param {string} exactVersion */
94
+ async function hasCachedPackage(manifestPath, exactVersion) {
95
+ try {
96
+ const installed = JSON.parse(await readFile(manifestPath, 'utf8'));
97
+ return installed.version === exactVersion;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /** @param {string} command @param {string[]} args @param {string} cwd */
104
+ function runCommand(command, args, cwd) {
105
+ return new Promise((resolve, reject) => {
106
+ const child = spawn(command, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
107
+ child.on('error', reject);
108
+ child.on('close', code => resolve(code ?? 1));
109
+ });
110
+ }
@@ -5,4 +5,4 @@ export default async function getGitRoot() {
5
5
  const exec = util.promisify(childProcess.exec);
6
6
  const { stdout } = await exec('git rev-parse --show-toplevel');
7
7
  return stdout.trim();
8
- }
8
+ }