@yopem-ui/cli 0.1.0 → 0.1.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/package.json +1 -1
- package/src/cli.ts +41 -6
- package/src/init.ts +254 -70
- package/src/install.ts +147 -17
- package/src/project.ts +172 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { installItem, isRecord, isString } from "@yopem-ui/cli/install"
|
|
|
8
8
|
import { resolve } from "node:path"
|
|
9
9
|
|
|
10
10
|
const usage =
|
|
11
|
-
"Usage: yopem-ui [--help | --version] | init [--framework vite|tanstack-router|tanstack-start|react-router|next|astro] [--ui <path>] [--cwd <path>] [--registry <URL>] | <add|update> <name> [--force] [--dry-run] [--cwd <path>] [--registry <URL>]"
|
|
11
|
+
"Usage: yopem-ui [--help | --version] | init [--framework vite|tanstack-router|tanstack-start|react-router|next|astro] [--ui <path>] [--dry-run] [--cwd <path>] [--registry <URL>] | <add|update> <name> [--force] [--dry-run] [--cwd <path>] [--registry <URL>]"
|
|
12
12
|
|
|
13
13
|
function isFramework(
|
|
14
14
|
value: string | undefined,
|
|
@@ -78,9 +78,12 @@ export async function runCli(args: string[], options: InitOptions = {}) {
|
|
|
78
78
|
const cwd = resolve(options.cwd ?? process.cwd(), flags.get("--cwd") ?? ".")
|
|
79
79
|
const registryUrl = flags.get("--registry") ?? options.registryUrl
|
|
80
80
|
|
|
81
|
+
function onWarning(warning: string) {
|
|
82
|
+
console.warn(`Warning: ${warning}`)
|
|
83
|
+
options.onWarning?.(warning)
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
if (command === "init") {
|
|
82
|
-
if (flags.has("--dry-run") || options.dryRun)
|
|
83
|
-
throw new Error("--dry-run is not supported for init")
|
|
84
87
|
const framework = flags.get("--framework")
|
|
85
88
|
|
|
86
89
|
if (
|
|
@@ -94,13 +97,44 @@ export async function runCli(args: string[], options: InitOptions = {}) {
|
|
|
94
97
|
...options,
|
|
95
98
|
cwd,
|
|
96
99
|
registryUrl,
|
|
100
|
+
dryRun: flags.has("--dry-run") || options.dryRun,
|
|
101
|
+
onWarning,
|
|
97
102
|
ui: flags.get("--ui") ?? options.ui,
|
|
98
103
|
framework: isFramework(framework) ? framework : options.framework,
|
|
99
104
|
})
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
if (result.preview) {
|
|
107
|
+
for (const file of result.preview.files) {
|
|
108
|
+
const label =
|
|
109
|
+
file.action === "write"
|
|
110
|
+
? "Write"
|
|
111
|
+
: file.action === "delete"
|
|
112
|
+
? "Delete"
|
|
113
|
+
: "Skip"
|
|
114
|
+
|
|
115
|
+
console.info(
|
|
116
|
+
`${label} ${file.path}${file.reason ? ` (${file.reason})` : ""}`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const group of result.preview.dependencies) {
|
|
121
|
+
console.info(
|
|
122
|
+
`Runtime dependencies (${group.cwd}): ${group.dependencies.join(", ") || "none"}`,
|
|
123
|
+
)
|
|
124
|
+
console.info(
|
|
125
|
+
`Dev dependencies (${group.cwd}): ${group.devDependencies.join(", ") || "none"}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.info(`Prerequisites: ${result.preview.prerequisites.join("; ")}`)
|
|
130
|
+
console.info(
|
|
131
|
+
`Dry run: ${result.framework}. No files written or package-manager commands run.`,
|
|
132
|
+
)
|
|
133
|
+
} else {
|
|
134
|
+
console.info(
|
|
135
|
+
`Configured ${result.framework} (${result.configured} file(s))`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
104
138
|
|
|
105
139
|
return result
|
|
106
140
|
}
|
|
@@ -119,6 +153,7 @@ export async function runCli(args: string[], options: InitOptions = {}) {
|
|
|
119
153
|
cwd,
|
|
120
154
|
registryUrl,
|
|
121
155
|
mode: command,
|
|
156
|
+
onWarning,
|
|
122
157
|
force: flags.has("--force"),
|
|
123
158
|
dryRun: flags.has("--dry-run") || options.dryRun,
|
|
124
159
|
})
|
package/src/init.ts
CHANGED
|
@@ -6,15 +6,17 @@ import type {
|
|
|
6
6
|
} from "@yopem-ui/cli/install"
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
applyInstall,
|
|
9
10
|
existingFile,
|
|
10
|
-
installItem,
|
|
11
11
|
isRecord,
|
|
12
12
|
isString,
|
|
13
|
+
planInstall,
|
|
13
14
|
writeFiles,
|
|
14
15
|
} from "@yopem-ui/cli/install"
|
|
15
16
|
import {
|
|
16
17
|
isPackageName,
|
|
17
18
|
packageRunner,
|
|
19
|
+
pendingDependencies,
|
|
18
20
|
workspaceRoot,
|
|
19
21
|
} from "@yopem-ui/cli/project"
|
|
20
22
|
import { readFile, realpath } from "node:fs/promises"
|
|
@@ -40,6 +42,25 @@ interface Edit {
|
|
|
40
42
|
text: string
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
export interface InitResult {
|
|
46
|
+
framework: Framework
|
|
47
|
+
configured: number
|
|
48
|
+
warnings?: string[]
|
|
49
|
+
preview?: {
|
|
50
|
+
files: {
|
|
51
|
+
path: string
|
|
52
|
+
action: "write" | "skip" | "delete" | "conflict"
|
|
53
|
+
reason?: string
|
|
54
|
+
}[]
|
|
55
|
+
dependencies: {
|
|
56
|
+
cwd: string
|
|
57
|
+
dependencies: string[]
|
|
58
|
+
devDependencies: string[]
|
|
59
|
+
}[]
|
|
60
|
+
prerequisites: string[]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
export interface InitOptions extends InstallOptions {
|
|
44
65
|
framework?: Framework
|
|
45
66
|
ui?: string
|
|
@@ -1547,8 +1568,9 @@ function detectFramework(
|
|
|
1547
1568
|
return detected[0]!
|
|
1548
1569
|
}
|
|
1549
1570
|
|
|
1550
|
-
export async function initProject(
|
|
1551
|
-
|
|
1571
|
+
export async function initProject(
|
|
1572
|
+
options: InitOptions = {},
|
|
1573
|
+
): Promise<InitResult> {
|
|
1552
1574
|
const root = await realpath(options.cwd ?? process.cwd())
|
|
1553
1575
|
|
|
1554
1576
|
if (!(await existingFile(root, "package.json"))) {
|
|
@@ -1601,10 +1623,17 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1601
1623
|
const uiRun = await packageRunner(uiRoot, options.run)
|
|
1602
1624
|
const edits = new Map<string, { before: string | null; after: string }>()
|
|
1603
1625
|
|
|
1626
|
+
const plannedFiles: {
|
|
1627
|
+
path: string
|
|
1628
|
+
action: "write" | "skip" | "delete"
|
|
1629
|
+
reason?: string
|
|
1630
|
+
}[] = []
|
|
1631
|
+
|
|
1604
1632
|
async function plan(
|
|
1605
1633
|
path: string,
|
|
1606
1634
|
transform: (content: string) => string,
|
|
1607
1635
|
fallback?: string,
|
|
1636
|
+
reason?: string,
|
|
1608
1637
|
) {
|
|
1609
1638
|
const exists = await existingFile(root, path)
|
|
1610
1639
|
|
|
@@ -1612,6 +1641,12 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1612
1641
|
const before = exists ? await readFile(join(root, path), "utf8") : null
|
|
1613
1642
|
const after = transform(before ?? fallback!)
|
|
1614
1643
|
|
|
1644
|
+
plannedFiles.push({
|
|
1645
|
+
path: resolve(root, path),
|
|
1646
|
+
action: before === after ? "skip" : "write",
|
|
1647
|
+
...(reason && { reason }),
|
|
1648
|
+
})
|
|
1649
|
+
|
|
1615
1650
|
if (before !== after) edits.set(path, { before, after })
|
|
1616
1651
|
}
|
|
1617
1652
|
|
|
@@ -1669,43 +1704,48 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1669
1704
|
if (shared) {
|
|
1670
1705
|
const prefix = relative(root, uiRoot).replaceAll("\\", "/")
|
|
1671
1706
|
|
|
1672
|
-
await plan(
|
|
1673
|
-
|
|
1707
|
+
await plan(
|
|
1708
|
+
`${prefix}/package.json`,
|
|
1709
|
+
(content) => {
|
|
1710
|
+
const value: JsonValue = JSON.parse(content)
|
|
1674
1711
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1712
|
+
if (
|
|
1713
|
+
!object(value) ||
|
|
1714
|
+
(value.exports !== undefined && !object(value.exports))
|
|
1715
|
+
)
|
|
1716
|
+
throw new Error("Shared UI package requires object exports")
|
|
1717
|
+
const exports = object(value.exports) ? { ...value.exports } : {}
|
|
1718
|
+
|
|
1719
|
+
const paths = {
|
|
1720
|
+
"./components/ui/*": "./src/components/ui/*.tsx",
|
|
1721
|
+
"./lib/*": "./src/lib/*.ts",
|
|
1722
|
+
"./styles/tokens.stylex": "./src/styles/tokens.stylex.ts",
|
|
1723
|
+
"./styles/styles.css": "./src/styles/styles.css",
|
|
1724
|
+
"./theme/*": "./src/theme/*.tsx",
|
|
1725
|
+
}
|
|
1689
1726
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1727
|
+
for (const [path, target] of Object.entries(paths)) {
|
|
1728
|
+
if (exports[path] !== undefined && exports[path] !== target)
|
|
1729
|
+
throw new Error(`Conflicting UI package export: ${path}`)
|
|
1730
|
+
exports[path] = target
|
|
1731
|
+
}
|
|
1695
1732
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1733
|
+
const sideEffects =
|
|
1734
|
+
value.sideEffects === false
|
|
1735
|
+
? ["**/*.css"]
|
|
1736
|
+
: Array.isArray(value.sideEffects) &&
|
|
1737
|
+
!value.sideEffects.includes("**/*.css")
|
|
1738
|
+
? [...value.sideEffects, "**/*.css"]
|
|
1739
|
+
: value.sideEffects
|
|
1740
|
+
|
|
1741
|
+
return JSON.stringify(value.exports) === JSON.stringify(exports) &&
|
|
1742
|
+
JSON.stringify(value.sideEffects) === JSON.stringify(sideEffects)
|
|
1743
|
+
? content
|
|
1744
|
+
: `${JSON.stringify({ ...value, exports, sideEffects }, null, 2)}\n`
|
|
1745
|
+
},
|
|
1746
|
+
undefined,
|
|
1747
|
+
"Shared UI exports: ./components/ui/*, ./lib/*, ./styles/tokens.stylex, ./styles/styles.css, ./theme/*; CSS side effects",
|
|
1748
|
+
)
|
|
1709
1749
|
}
|
|
1710
1750
|
|
|
1711
1751
|
const devDependencies = [
|
|
@@ -1868,15 +1908,165 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1868
1908
|
}
|
|
1869
1909
|
}
|
|
1870
1910
|
|
|
1911
|
+
const basePlan = await planInstall("base", {
|
|
1912
|
+
...options,
|
|
1913
|
+
cwd: uiRoot,
|
|
1914
|
+
importPrefix: shared?.name,
|
|
1915
|
+
dryRun: true,
|
|
1916
|
+
run: uiRun,
|
|
1917
|
+
})
|
|
1918
|
+
|
|
1919
|
+
const conflict = basePlan.preview.files.find(
|
|
1920
|
+
(file) => file.action === "conflict",
|
|
1921
|
+
)
|
|
1922
|
+
|
|
1923
|
+
if (conflict) throw new Error(conflict.reason)
|
|
1924
|
+
|
|
1925
|
+
if (
|
|
1926
|
+
!basePlan.changes.get("src/styles/styles.css")?.after?.includes("@stylex;")
|
|
1927
|
+
) {
|
|
1928
|
+
throw new Error(
|
|
1929
|
+
"Add @stylex; to src/styles/styles.css before configuring StyleX",
|
|
1930
|
+
)
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const runtimeRequirements =
|
|
1934
|
+
uiRoot === root
|
|
1935
|
+
? [...basePlan.requirements.dependencies, ...runtimeDependencies]
|
|
1936
|
+
: runtimeDependencies
|
|
1937
|
+
|
|
1938
|
+
const devRequirements =
|
|
1939
|
+
uiRoot === root
|
|
1940
|
+
? [...basePlan.requirements.devDependencies, ...devDependencies]
|
|
1941
|
+
: devDependencies
|
|
1942
|
+
|
|
1943
|
+
const appPending = await pendingDependencies(
|
|
1944
|
+
root,
|
|
1945
|
+
runtimeRequirements,
|
|
1946
|
+
devRequirements,
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
const baseDependencyNames = new Set(
|
|
1950
|
+
[
|
|
1951
|
+
...basePlan.requirements.dependencies,
|
|
1952
|
+
...basePlan.requirements.devDependencies,
|
|
1953
|
+
].map((argument) => {
|
|
1954
|
+
const separator = argument.indexOf("@", 1)
|
|
1955
|
+
|
|
1956
|
+
return separator < 0 ? argument : argument.slice(0, separator)
|
|
1957
|
+
}),
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
function isBaseDependency(argument: string) {
|
|
1961
|
+
const separator = argument.indexOf("@", 1)
|
|
1962
|
+
|
|
1963
|
+
return baseDependencyNames.has(
|
|
1964
|
+
separator < 0 ? argument : argument.slice(0, separator),
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
const baseApplicationPlan =
|
|
1969
|
+
uiRoot === root
|
|
1970
|
+
? {
|
|
1971
|
+
...basePlan,
|
|
1972
|
+
preview: {
|
|
1973
|
+
...basePlan.preview,
|
|
1974
|
+
dependencies: appPending.dependencies.filter(isBaseDependency),
|
|
1975
|
+
devDependencies:
|
|
1976
|
+
appPending.devDependencies.filter(isBaseDependency),
|
|
1977
|
+
},
|
|
1978
|
+
}
|
|
1979
|
+
: basePlan
|
|
1980
|
+
|
|
1981
|
+
if (options.dryRun) {
|
|
1982
|
+
const baseDependencies = {
|
|
1983
|
+
dependencies: basePlan.preview.dependencies,
|
|
1984
|
+
devDependencies: basePlan.preview.devDependencies,
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
for (const [path] of retired)
|
|
1988
|
+
plannedFiles.push({ path: resolve(root, path), action: "delete" })
|
|
1989
|
+
|
|
1990
|
+
if (framework === "next") {
|
|
1991
|
+
if (!object(manifest.scripts))
|
|
1992
|
+
throw new Error("Next.js scripts are missing")
|
|
1993
|
+
const originalScripts = manifest.scripts
|
|
1994
|
+
const scripts = nextScripts(originalScripts)
|
|
1995
|
+
plannedFiles.push({
|
|
1996
|
+
path: join(root, "package.json"),
|
|
1997
|
+
action:
|
|
1998
|
+
JSON.stringify(scripts) === JSON.stringify(manifest.scripts)
|
|
1999
|
+
? "skip"
|
|
2000
|
+
: "write",
|
|
2001
|
+
reason: `Next.js scripts: ${
|
|
2002
|
+
Object.entries(scripts)
|
|
2003
|
+
.filter(([name, value]) => value !== originalScripts[name])
|
|
2004
|
+
.map(
|
|
2005
|
+
([name, value]) =>
|
|
2006
|
+
`${name}: ${JSON.stringify(originalScripts[name])} -> ${JSON.stringify(value)}`,
|
|
2007
|
+
)
|
|
2008
|
+
.join("; ") || "unchanged"
|
|
2009
|
+
}`,
|
|
2010
|
+
})
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const dependencyTargets =
|
|
2014
|
+
uiRoot === root
|
|
2015
|
+
? [{ cwd: root, ...appPending }]
|
|
2016
|
+
: [
|
|
2017
|
+
{ cwd: uiRoot, ...baseDependencies },
|
|
2018
|
+
{ cwd: root, ...appPending },
|
|
2019
|
+
]
|
|
2020
|
+
|
|
2021
|
+
for (const group of dependencyTargets) {
|
|
2022
|
+
if (!group.dependencies.length && !group.devDependencies.length) continue
|
|
2023
|
+
const path = join(group.cwd, "package.json")
|
|
2024
|
+
const planned = plannedFiles.find((file) => file.path === path)
|
|
2025
|
+
|
|
2026
|
+
if (planned) {
|
|
2027
|
+
planned.action = "write"
|
|
2028
|
+
planned.reason = [
|
|
2029
|
+
planned.reason,
|
|
2030
|
+
"Pending package-manager dependency changes",
|
|
2031
|
+
]
|
|
2032
|
+
.filter(Boolean)
|
|
2033
|
+
.join("; ")
|
|
2034
|
+
} else {
|
|
2035
|
+
plannedFiles.push({
|
|
2036
|
+
path,
|
|
2037
|
+
action: "write",
|
|
2038
|
+
reason: "Pending package-manager dependency changes",
|
|
2039
|
+
})
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
for (const warning of basePlan.warnings) options.onWarning?.(warning)
|
|
2044
|
+
|
|
2045
|
+
return {
|
|
2046
|
+
framework,
|
|
2047
|
+
configured: 0,
|
|
2048
|
+
...(basePlan.warnings.length && { warnings: basePlan.warnings }),
|
|
2049
|
+
preview: {
|
|
2050
|
+
files: [
|
|
2051
|
+
...basePlan.preview.files.map((file) => ({
|
|
2052
|
+
...file,
|
|
2053
|
+
path: join(uiRoot, file.path),
|
|
2054
|
+
})),
|
|
2055
|
+
...plannedFiles,
|
|
2056
|
+
],
|
|
2057
|
+
dependencies: dependencyTargets,
|
|
2058
|
+
prerequisites: [
|
|
2059
|
+
"Supported framework, safe project paths and configuration",
|
|
2060
|
+
"Validated registry base with @stylex; stylesheet directive",
|
|
2061
|
+
],
|
|
2062
|
+
},
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1871
2066
|
let baseInstalled = false
|
|
1872
2067
|
|
|
1873
2068
|
try {
|
|
1874
|
-
await
|
|
1875
|
-
...options,
|
|
1876
|
-
cwd: uiRoot,
|
|
1877
|
-
importPrefix: shared?.name,
|
|
1878
|
-
run: uiRun,
|
|
1879
|
-
})
|
|
2069
|
+
const baseResult = await applyInstall(baseApplicationPlan, options)
|
|
1880
2070
|
baseInstalled = true
|
|
1881
2071
|
|
|
1882
2072
|
if (
|
|
@@ -1897,36 +2087,26 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1897
2087
|
if (!object(installed))
|
|
1898
2088
|
throw new Error("Invalid package.json after installation")
|
|
1899
2089
|
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2090
|
+
const pending = await pendingDependencies(
|
|
2091
|
+
root,
|
|
2092
|
+
runtimeRequirements,
|
|
2093
|
+
devRequirements,
|
|
1904
2094
|
)
|
|
1905
2095
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
const packageName = name.includes("@", 1)
|
|
1917
|
-
? name.slice(0, name.lastIndexOf("@"))
|
|
1918
|
-
: name
|
|
1919
|
-
|
|
1920
|
-
return !(packageName in available)
|
|
1921
|
-
})
|
|
1922
|
-
|
|
1923
|
-
if (neededRuntime.length) await packageRun(["add", ...neededRuntime], root)
|
|
2096
|
+
if (uiRoot === root) {
|
|
2097
|
+
pending.dependencies = pending.dependencies.filter(
|
|
2098
|
+
(argument) => !isBaseDependency(argument),
|
|
2099
|
+
)
|
|
2100
|
+
pending.devDependencies = pending.devDependencies.filter(
|
|
2101
|
+
(argument) => !isBaseDependency(argument),
|
|
2102
|
+
)
|
|
2103
|
+
}
|
|
1924
2104
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
)
|
|
2105
|
+
if (pending.dependencies.length)
|
|
2106
|
+
await packageRun(["add", ...pending.dependencies], root)
|
|
1928
2107
|
|
|
1929
|
-
if (
|
|
2108
|
+
if (pending.devDependencies.length)
|
|
2109
|
+
await packageRun(["add", "-d", ...pending.devDependencies], root)
|
|
1930
2110
|
|
|
1931
2111
|
if (shared) {
|
|
1932
2112
|
const path = `${relative(root, uiRoot).replaceAll("\\", "/")}/package.json`
|
|
@@ -1990,7 +2170,11 @@ export async function initProject(options: InitOptions = {}) {
|
|
|
1990
2170
|
|
|
1991
2171
|
await writeFiles(root, changes)
|
|
1992
2172
|
|
|
1993
|
-
return {
|
|
2173
|
+
return {
|
|
2174
|
+
framework,
|
|
2175
|
+
configured: edits.size,
|
|
2176
|
+
...(baseResult.warnings && { warnings: baseResult.warnings }),
|
|
2177
|
+
}
|
|
1994
2178
|
} catch (cause) {
|
|
1995
2179
|
if (!baseInstalled) throw cause
|
|
1996
2180
|
throw new Error(
|
package/src/install.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isPackageName,
|
|
3
|
+
packageRunner,
|
|
4
|
+
pendingDependencies,
|
|
5
|
+
} from "@yopem-ui/cli/project"
|
|
2
6
|
import { createHash } from "node:crypto"
|
|
3
7
|
import {
|
|
4
8
|
chmod,
|
|
@@ -55,12 +59,19 @@ interface RegistryItem {
|
|
|
55
59
|
files: RegistryFile[]
|
|
56
60
|
name: string
|
|
57
61
|
registryDependencies: string[]
|
|
62
|
+
registryVersion: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Provenance {
|
|
66
|
+
registryUrl: string
|
|
67
|
+
items: Record<string, string>
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
interface Manifest {
|
|
61
71
|
version: 1
|
|
62
72
|
files: Record<string, string>
|
|
63
73
|
importPrefix?: string
|
|
74
|
+
provenance?: Record<string, Provenance>
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
export interface InstallPreview {
|
|
@@ -74,6 +85,13 @@ export interface InstallPreview {
|
|
|
74
85
|
devDependencies: string[]
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
export interface InstallResult {
|
|
89
|
+
installed: number
|
|
90
|
+
skipped: number
|
|
91
|
+
preview?: InstallPreview
|
|
92
|
+
warnings?: string[]
|
|
93
|
+
}
|
|
94
|
+
|
|
77
95
|
export interface InstallOptions {
|
|
78
96
|
cwd?: string
|
|
79
97
|
dryRun?: boolean
|
|
@@ -84,6 +102,7 @@ export interface InstallOptions {
|
|
|
84
102
|
requestTimeoutMs?: number
|
|
85
103
|
fetcher?: (url: string, init?: RequestInit) => Promise<Response>
|
|
86
104
|
run?: (args: string[], cwd: string) => Promise<void>
|
|
105
|
+
onWarning?: (warning: string) => void
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
function registryBaseUrl(value: string) {
|
|
@@ -132,13 +151,22 @@ async function registryItemJson(
|
|
|
132
151
|
let response: Response
|
|
133
152
|
|
|
134
153
|
try {
|
|
135
|
-
response = await fetcher(url, {
|
|
154
|
+
response = await fetcher(url, {
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
redirect: "error",
|
|
157
|
+
})
|
|
136
158
|
} catch {
|
|
137
159
|
throw new Error(
|
|
138
160
|
`Registry network error: ${url}. Check your connection and --registry URL.`,
|
|
139
161
|
)
|
|
140
162
|
}
|
|
141
163
|
|
|
164
|
+
if (response.status >= 300 && response.status < 400) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Registry redirect rejected: ${url} (HTTP ${response.status}). Redirects are not allowed; check --registry URL.`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
142
170
|
if (!response.ok) {
|
|
143
171
|
throw new Error(
|
|
144
172
|
`Registry request failed: ${url} (HTTP ${response.status}). Check --registry URL and item name.`,
|
|
@@ -226,6 +254,7 @@ function parseItem(value: JsonValue, name: string): RegistryItem {
|
|
|
226
254
|
!isString(value.title) ||
|
|
227
255
|
!isString(value.description) ||
|
|
228
256
|
!isString(value.registryVersion) ||
|
|
257
|
+
!value.registryVersion.trim() ||
|
|
229
258
|
!isStringArray(value.categories) ||
|
|
230
259
|
!isStringArray(value.dependencies) ||
|
|
231
260
|
!isStringArray(value.devDependencies) ||
|
|
@@ -291,6 +320,7 @@ function parseItem(value: JsonValue, name: string): RegistryItem {
|
|
|
291
320
|
dependencies: value.dependencies,
|
|
292
321
|
devDependencies: value.devDependencies,
|
|
293
322
|
registryDependencies: value.registryDependencies,
|
|
323
|
+
registryVersion: value.registryVersion,
|
|
294
324
|
}
|
|
295
325
|
}
|
|
296
326
|
|
|
@@ -353,9 +383,44 @@ function readManifest(content: string | null): Manifest {
|
|
|
353
383
|
files[path] = integrity
|
|
354
384
|
}
|
|
355
385
|
|
|
386
|
+
const provenance: Record<string, Provenance> = {}
|
|
387
|
+
|
|
388
|
+
if (value.provenance !== undefined) {
|
|
389
|
+
if (!isRecord(value.provenance)) throw new Error("Invalid ui.json")
|
|
390
|
+
|
|
391
|
+
for (const [path, origin] of Object.entries(value.provenance)) {
|
|
392
|
+
if (
|
|
393
|
+
!files[path] ||
|
|
394
|
+
!isRecord(origin) ||
|
|
395
|
+
!isString(origin.registryUrl) ||
|
|
396
|
+
!isRecord(origin.items) ||
|
|
397
|
+
Object.keys(origin.items).length === 0
|
|
398
|
+
)
|
|
399
|
+
throw new Error("Invalid ui.json")
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
if (registryBaseUrl(origin.registryUrl) !== origin.registryUrl)
|
|
403
|
+
throw new Error("Invalid ui.json")
|
|
404
|
+
} catch {
|
|
405
|
+
throw new Error("Invalid ui.json provenance registry URL")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const items: Record<string, string> = {}
|
|
409
|
+
|
|
410
|
+
for (const [name, version] of Object.entries(origin.items)) {
|
|
411
|
+
if (!namePattern.test(name) || !isString(version) || !version.trim())
|
|
412
|
+
throw new Error("Invalid ui.json")
|
|
413
|
+
items[name] = version
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
provenance[path] = { registryUrl: origin.registryUrl, items }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
356
420
|
return {
|
|
357
421
|
version: 1,
|
|
358
422
|
files,
|
|
423
|
+
...(value.provenance !== undefined && { provenance }),
|
|
359
424
|
...(value.importPrefix !== undefined && {
|
|
360
425
|
importPrefix: value.importPrefix,
|
|
361
426
|
}),
|
|
@@ -549,7 +614,7 @@ export async function runBun(args: string[], cwd: string) {
|
|
|
549
614
|
}
|
|
550
615
|
}
|
|
551
616
|
|
|
552
|
-
export async function
|
|
617
|
+
export async function planInstall(name: string, options: InstallOptions = {}) {
|
|
553
618
|
if (!namePattern.test(name)) throw new Error(`Invalid item name: ${name}`)
|
|
554
619
|
const registryUrl = registryBaseUrl(options.registryUrl ?? defaultRegistryUrl)
|
|
555
620
|
const requestTimeoutMs = options.requestTimeoutMs ?? 30_000
|
|
@@ -572,6 +637,7 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
572
637
|
: null
|
|
573
638
|
|
|
574
639
|
const manifest = readManifest(manifestText)
|
|
640
|
+
const originalManifest = JSON.stringify(manifest)
|
|
575
641
|
const importPrefix = options.importPrefix ?? manifest.importPrefix ?? "@"
|
|
576
642
|
|
|
577
643
|
if (
|
|
@@ -619,6 +685,7 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
619
685
|
|
|
620
686
|
await visit(name)
|
|
621
687
|
const files = new Map<string, RegistryFile>()
|
|
688
|
+
const owners = new Map<string, Record<string, string>>()
|
|
622
689
|
const dependencies = new Set<string>()
|
|
623
690
|
const devDependencies = new Set<string>()
|
|
624
691
|
|
|
@@ -639,6 +706,9 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
639
706
|
}
|
|
640
707
|
|
|
641
708
|
files.set(path, transformed)
|
|
709
|
+
const ownership = owners.get(path) ?? {}
|
|
710
|
+
ownership[item.name] = item.registryVersion
|
|
711
|
+
owners.set(path, ownership)
|
|
642
712
|
}
|
|
643
713
|
}
|
|
644
714
|
|
|
@@ -646,6 +716,26 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
646
716
|
const previewFiles: InstallPreview["files"] = []
|
|
647
717
|
let installed = 0
|
|
648
718
|
let skipped = 0
|
|
719
|
+
const warnings: string[] = []
|
|
720
|
+
|
|
721
|
+
function track(path: string, integrity: string, applied: boolean) {
|
|
722
|
+
manifest.files[path] = integrity
|
|
723
|
+
const previous = manifest.provenance?.[path]
|
|
724
|
+
|
|
725
|
+
if (!applied && previous && previous.registryUrl !== registryUrl) return
|
|
726
|
+
|
|
727
|
+
const ownership =
|
|
728
|
+
applied || !previous ? { ...owners.get(path)! } : { ...previous.items }
|
|
729
|
+
|
|
730
|
+
if (!applied && previous) {
|
|
731
|
+
for (const [name, version] of Object.entries(owners.get(path)!)) {
|
|
732
|
+
if (!Object.hasOwn(ownership, name)) ownership[name] = version
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
manifest.provenance ??= {}
|
|
737
|
+
manifest.provenance[path] = { registryUrl, items: ownership }
|
|
738
|
+
}
|
|
649
739
|
|
|
650
740
|
for (const [path, file] of files) {
|
|
651
741
|
const exists = await existingFile(root, path)
|
|
@@ -654,6 +744,14 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
654
744
|
const current = before === null ? null : hash(before)
|
|
655
745
|
changes.set(path, { before, after: before })
|
|
656
746
|
const previous = manifest.files[path]
|
|
747
|
+
const origin = manifest.provenance?.[path]
|
|
748
|
+
|
|
749
|
+
if (origin && origin.registryUrl !== registryUrl) {
|
|
750
|
+
warnings.push(
|
|
751
|
+
`Updating tracked file from a different registry: ${path}. Previous: ${origin.registryUrl}; requested: ${registryUrl}. Review source before applying.`,
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
|
|
657
755
|
const decision: InstallPreview["files"][number] = { path, action: "skip" }
|
|
658
756
|
previewFiles.push(decision)
|
|
659
757
|
|
|
@@ -670,7 +768,7 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
670
768
|
}
|
|
671
769
|
|
|
672
770
|
if (current === file.integrity) {
|
|
673
|
-
|
|
771
|
+
track(path, file.integrity, false)
|
|
674
772
|
decision.reason = `Identical file: ${path}`
|
|
675
773
|
skipped++
|
|
676
774
|
} else if (
|
|
@@ -691,29 +789,61 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
691
789
|
|
|
692
790
|
if (current && options.force) decision.forced = true
|
|
693
791
|
changes.set(path, { before, after: file.content })
|
|
694
|
-
|
|
792
|
+
track(path, file.integrity, true)
|
|
695
793
|
installed++
|
|
696
794
|
}
|
|
697
795
|
}
|
|
698
796
|
|
|
699
|
-
const manifestAfter =
|
|
797
|
+
const manifestAfter =
|
|
798
|
+
manifestText !== null && JSON.stringify(manifest) === originalManifest
|
|
799
|
+
? manifestText
|
|
800
|
+
: `${JSON.stringify(manifest, null, 2)}\n`
|
|
801
|
+
|
|
700
802
|
changes.set("ui.json", { before: manifestText, after: manifestAfter })
|
|
701
803
|
previewFiles.push({
|
|
702
804
|
path: "ui.json",
|
|
703
805
|
action: manifestText === manifestAfter ? "skip" : "write",
|
|
704
806
|
})
|
|
705
807
|
|
|
706
|
-
|
|
808
|
+
const pending = await pendingDependencies(root, dependencies, devDependencies)
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
root,
|
|
812
|
+
run,
|
|
813
|
+
changes,
|
|
814
|
+
installed,
|
|
815
|
+
skipped,
|
|
816
|
+
warnings,
|
|
817
|
+
requirements: {
|
|
818
|
+
dependencies: [...dependencies],
|
|
819
|
+
devDependencies: [...devDependencies],
|
|
820
|
+
},
|
|
821
|
+
preview: { files: previewFiles, ...pending },
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export async function installItem(
|
|
826
|
+
name: string,
|
|
827
|
+
options: InstallOptions = {},
|
|
828
|
+
): Promise<InstallResult> {
|
|
829
|
+
return await applyInstall(await planInstall(name, options), options)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export async function applyInstall(
|
|
833
|
+
plan: Awaited<ReturnType<typeof planInstall>>,
|
|
834
|
+
options: InstallOptions = {},
|
|
835
|
+
): Promise<InstallResult> {
|
|
836
|
+
const { root, run, changes, installed, skipped, warnings, preview } = plan
|
|
837
|
+
const warningResult = warnings.length ? { warnings } : {}
|
|
838
|
+
|
|
839
|
+
for (const warning of warnings) options.onWarning?.(warning)
|
|
707
840
|
|
|
708
841
|
if (options.dryRun) {
|
|
709
842
|
return {
|
|
710
843
|
installed: 0,
|
|
711
844
|
skipped,
|
|
712
|
-
preview
|
|
713
|
-
|
|
714
|
-
dependencies: [...dependencies],
|
|
715
|
-
devDependencies: [...devDependencies],
|
|
716
|
-
},
|
|
845
|
+
preview,
|
|
846
|
+
...warningResult,
|
|
717
847
|
}
|
|
718
848
|
}
|
|
719
849
|
|
|
@@ -721,14 +851,14 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
721
851
|
|
|
722
852
|
try {
|
|
723
853
|
await writeFiles(root, changes, async () => {
|
|
724
|
-
if (dependencies.
|
|
854
|
+
if (preview.dependencies.length) {
|
|
725
855
|
dependenciesStarted = true
|
|
726
|
-
await run(["add", ...dependencies], root)
|
|
856
|
+
await run(["add", ...preview.dependencies], root)
|
|
727
857
|
}
|
|
728
858
|
|
|
729
|
-
if (devDependencies.
|
|
859
|
+
if (preview.devDependencies.length) {
|
|
730
860
|
dependenciesStarted = true
|
|
731
|
-
await run(["add", "-d", ...devDependencies], root)
|
|
861
|
+
await run(["add", "-d", ...preview.devDependencies], root)
|
|
732
862
|
}
|
|
733
863
|
})
|
|
734
864
|
} catch (cause) {
|
|
@@ -739,5 +869,5 @@ export async function installItem(name: string, options: InstallOptions = {}) {
|
|
|
739
869
|
)
|
|
740
870
|
}
|
|
741
871
|
|
|
742
|
-
return { installed, skipped }
|
|
872
|
+
return { installed, skipped, ...warningResult }
|
|
743
873
|
}
|
package/src/project.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function isPackageName(value: unknown): value is string {
|
|
|
21
21
|
)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
async function readPackage(root: string): Promise<JsonObject> {
|
|
24
|
+
export async function readPackage(root: string): Promise<JsonObject> {
|
|
25
25
|
if (!(await existingFile(root, "package.json"))) return {}
|
|
26
26
|
|
|
27
27
|
const value: JsonValue = JSON.parse(
|
|
@@ -221,3 +221,174 @@ export async function packageRunner(
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
interface PendingDependencies {
|
|
226
|
+
dependencies: string[]
|
|
227
|
+
devDependencies: string[]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface Requirement {
|
|
231
|
+
name: string
|
|
232
|
+
spec: string
|
|
233
|
+
argument: string
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function requirement(argument: string): Requirement {
|
|
237
|
+
const separator = argument.indexOf("@", 1)
|
|
238
|
+
const name = separator < 0 ? argument : argument.slice(0, separator)
|
|
239
|
+
const spec = separator < 0 ? "" : argument.slice(separator + 1)
|
|
240
|
+
|
|
241
|
+
if (!isPackageName(name) || (separator >= 0 && !spec)) {
|
|
242
|
+
throw new Error(`Invalid package dependency: ${argument}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { name, spec, argument }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function bounds(spec: string) {
|
|
249
|
+
const match = spec
|
|
250
|
+
.trim()
|
|
251
|
+
.match(/^(\^|~)?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/)
|
|
252
|
+
|
|
253
|
+
if (!match) return null
|
|
254
|
+
const major = Number(match[2])
|
|
255
|
+
const hasMinor = match[3] !== undefined && /^\d+$/.test(match[3])
|
|
256
|
+
const hasPatch = match[4] !== undefined && /^\d+$/.test(match[4])
|
|
257
|
+
|
|
258
|
+
if (!hasMinor && hasPatch) return null
|
|
259
|
+
const minor = hasMinor ? Number(match[3]) : 0
|
|
260
|
+
const patch = hasPatch ? Number(match[4]) : 0
|
|
261
|
+
const minimum = `${major}.${minor}.${patch}`
|
|
262
|
+
|
|
263
|
+
if (!Bun.semver.satisfies(minimum, spec)) return null
|
|
264
|
+
|
|
265
|
+
const maximum =
|
|
266
|
+
match[1] === "^"
|
|
267
|
+
? major > 0 || !hasMinor
|
|
268
|
+
? `${major + 1}.0.0`
|
|
269
|
+
: minor > 0 || !hasPatch
|
|
270
|
+
? `0.${minor + 1}.0`
|
|
271
|
+
: `0.0.${patch + 1}`
|
|
272
|
+
: !hasMinor
|
|
273
|
+
? `${major + 1}.0.0`
|
|
274
|
+
: match[1] === "~" || !hasPatch
|
|
275
|
+
? `${major}.${minor + 1}.0`
|
|
276
|
+
: null
|
|
277
|
+
|
|
278
|
+
return { minimum, maximum }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function compatible(existing: string, requested: string) {
|
|
282
|
+
if (!requested || requested === "*" || existing === requested) return true
|
|
283
|
+
|
|
284
|
+
if (requested === "workspace:*") {
|
|
285
|
+
return existing === "*" || existing.startsWith("workspace:")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const current = bounds(existing)
|
|
289
|
+
const wanted = bounds(requested)
|
|
290
|
+
|
|
291
|
+
if (!current) {
|
|
292
|
+
return (
|
|
293
|
+
/^v?\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(
|
|
294
|
+
existing,
|
|
295
|
+
) && Bun.semver.satisfies(existing, requested)
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!current.maximum) return Bun.semver.satisfies(current.minimum, requested)
|
|
300
|
+
|
|
301
|
+
if (!wanted?.maximum) return false
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
Bun.semver.satisfies(current.minimum, requested) &&
|
|
305
|
+
Bun.semver.order(current.maximum, wanted.maximum) <= 0
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function section(manifest: JsonObject, name: string) {
|
|
310
|
+
const value = manifest[name]
|
|
311
|
+
|
|
312
|
+
if (value === undefined) return {}
|
|
313
|
+
|
|
314
|
+
if (!isRecord(value)) throw new Error(`Invalid package.json ${name}`)
|
|
315
|
+
|
|
316
|
+
for (const [name, spec] of Object.entries(value)) {
|
|
317
|
+
if (!isPackageName(name) || !isString(spec) || !spec.trim()) {
|
|
318
|
+
throw new Error(`Invalid package.json dependency: ${name}`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function pendingDependencies(
|
|
326
|
+
root: string,
|
|
327
|
+
runtime: Iterable<string>,
|
|
328
|
+
dev: Iterable<string>,
|
|
329
|
+
) {
|
|
330
|
+
const manifest = await readPackage(root)
|
|
331
|
+
const dependencies = section(manifest, "dependencies")
|
|
332
|
+
const devDependencies = section(manifest, "devDependencies")
|
|
333
|
+
|
|
334
|
+
const groups = new Map<
|
|
335
|
+
string,
|
|
336
|
+
{ runtime: boolean; requirements: Requirement[] }
|
|
337
|
+
>()
|
|
338
|
+
|
|
339
|
+
for (const [arguments_, isRuntime] of [
|
|
340
|
+
[runtime, true],
|
|
341
|
+
[dev, false],
|
|
342
|
+
] as const) {
|
|
343
|
+
for (const argument of arguments_) {
|
|
344
|
+
const entry = requirement(argument)
|
|
345
|
+
|
|
346
|
+
const group = groups.get(entry.name) ?? {
|
|
347
|
+
runtime: false,
|
|
348
|
+
requirements: [],
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
group.runtime ||= isRuntime
|
|
352
|
+
group.requirements.push(entry)
|
|
353
|
+
groups.set(entry.name, group)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const pending: PendingDependencies = {
|
|
358
|
+
dependencies: [],
|
|
359
|
+
devDependencies: [],
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const [name, group] of groups) {
|
|
363
|
+
const installed = dependencies[name] ?? devDependencies[name]
|
|
364
|
+
|
|
365
|
+
const satisfied =
|
|
366
|
+
isString(installed) &&
|
|
367
|
+
group.requirements.every((entry) => compatible(installed, entry.spec))
|
|
368
|
+
|
|
369
|
+
if (satisfied && (!group.runtime || dependencies[name] !== undefined))
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
const chosen = group.requirements.find((candidate) =>
|
|
373
|
+
group.requirements.every((entry) =>
|
|
374
|
+
compatible(candidate.spec || "*", entry.spec),
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if (!chosen && !satisfied) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Conflicting package requirements: ${group.requirements.map((entry) => entry.argument).join(", ")}`,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const argument = satisfied ? `${name}@${installed}` : chosen!.argument
|
|
385
|
+
|
|
386
|
+
const target = group.runtime
|
|
387
|
+
? pending.dependencies
|
|
388
|
+
: pending.devDependencies
|
|
389
|
+
|
|
390
|
+
target.push(argument)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return pending
|
|
394
|
+
}
|