create-pkgbld 1.8.1 → 2.0.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/LICENSE +1 -1
- package/README.md +85 -10
- package/extensions-schema.json +53 -0
- package/extensions.json +33 -0
- package/index.js +2 -2
- package/lock-schema-v1.json +26 -0
- package/package.json +21 -9
- package/src/conflicts.js +21 -0
- package/src/diff.js +83 -0
- package/src/engine.js +120 -0
- package/src/extension-cache.js +110 -0
- package/src/get-git-root.js +8 -0
- package/src/index.js +257 -0
- package/src/install.js +59 -0
- package/src/inventory.js +161 -0
- package/src/package-names.js +25 -0
- package/src/package-operations.js +353 -0
- package/src/package-resolution.js +40 -0
- package/src/package-version.js +48 -0
- package/src/plugin-compatibility.js +79 -0
- package/src/project-changes.js +379 -0
- package/src/project-lock.js +74 -0
- package/src/registry.js +184 -0
- package/src/subcommands.js +345 -0
- package/src/tree.js +282 -0
- package/src/tui.js +167 -0
- package/src/types.js +28 -0
- package/src/update-engine.js +196 -0
- package/dist/index.mjs +0 -683
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,22 +1,97 @@
|
|
|
1
1
|
# create-pkgbld
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/extensions.json
ADDED
|
@@ -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
|
-
|
|
3
|
-
import('./
|
|
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,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "2.0.0",
|
|
3
3
|
"name": "create-pkgbld",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Konstantin Shutkin",
|
|
6
6
|
"bin": "./index.js",
|
|
7
|
-
"main": "./
|
|
7
|
+
"main": "./src/index.js",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
10
|
+
"src",
|
|
11
|
+
"index.js",
|
|
12
|
+
"extensions.json",
|
|
13
|
+
"extensions-schema.json",
|
|
14
|
+
"lock-schema-v1.json"
|
|
11
15
|
],
|
|
12
16
|
"engines": {
|
|
13
17
|
"node": ">=20"
|
|
@@ -26,12 +30,20 @@
|
|
|
26
30
|
"init"
|
|
27
31
|
],
|
|
28
32
|
"dependencies": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"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",
|
|
33
36
|
"prompts": "2.4.2",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
37
|
+
"semver": "7.8.5",
|
|
38
|
+
"pkgbld": "2.0.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'"
|
|
36
48
|
}
|
|
37
49
|
}
|
package/src/conflicts.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import childProcess from 'node:child_process';
|
|
2
|
+
import util from 'node:util';
|
|
3
|
+
|
|
4
|
+
export default async function getGitRoot() {
|
|
5
|
+
const exec = util.promisify(childProcess.exec);
|
|
6
|
+
const { stdout } = await exec('git rev-parse --show-toplevel');
|
|
7
|
+
return stdout.trim();
|
|
8
|
+
}
|