@nuasite/cli 0.17.1 → 0.18.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/README.md +23 -0
- package/dist/index.js +306 -62
- package/dist/types/clean.d.ts +2 -13
- package/dist/types/clean.d.ts.map +1 -1
- package/dist/types/init.d.ts +20 -0
- package/dist/types/init.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/utils.d.ts +21 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clean.ts +4 -101
- package/src/index.ts +10 -0
- package/src/init.ts +260 -0
- package/src/utils.ts +109 -0
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
+
export interface CommandOptions {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
yes?: boolean;
|
|
5
|
+
}
|
|
1
6
|
export declare function findAstroConfig(cwd?: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Find the matching closing brace/bracket/paren, aware of string literals.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findMatchingClose(text: string, start: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Extract the text between the outermost { } of defineConfig({ ... })
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractConfigBody(content: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Remove a top-level property from an object literal body text.
|
|
17
|
+
*/
|
|
18
|
+
export declare function removeProperty(body: string, propName: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Assemble a complete config file from import lines and a body string.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assembleConfig(imports: string[], body: string): string;
|
|
2
23
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAc;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,OAAO,CAAA;CACb;AASD,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAM1E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA+BrE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgCrE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAWtE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuasite/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"prepack": "bun run ../../scripts/workspace-deps/resolve-deps.ts"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@nuasite/agent-summary": "0.
|
|
26
|
+
"@nuasite/agent-summary": "0.18.0",
|
|
27
27
|
"astro": "^6.0.2",
|
|
28
28
|
"stacktracey": "2.1.8"
|
|
29
29
|
},
|
package/src/clean.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
|
-
import { findAstroConfig } from './utils'
|
|
3
|
+
import { assembleConfig, extractConfigBody, findAstroConfig, removeProperty } from './utils'
|
|
4
|
+
import type { CommandOptions } from './utils'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
cwd?: string
|
|
7
|
-
dryRun?: boolean
|
|
8
|
-
yes?: boolean
|
|
9
|
-
}
|
|
6
|
+
export type CleanOptions = CommandOptions
|
|
10
7
|
|
|
11
8
|
export type FeatureKey = 'cms' | 'pageMarkdown' | 'mdx' | 'sitemap' | 'tailwindcss' | 'checks'
|
|
12
9
|
const FEATURE_KEYS: FeatureKey[] = ['cms', 'pageMarkdown', 'mdx', 'sitemap', 'tailwindcss', 'checks']
|
|
@@ -43,93 +40,6 @@ export function detectDisabledFeatures(content: string): Set<FeatureKey> {
|
|
|
43
40
|
return disabled
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
/**
|
|
47
|
-
* Find the matching closing brace/bracket/paren, aware of string literals.
|
|
48
|
-
*/
|
|
49
|
-
function findMatchingClose(text: string, start: number): number {
|
|
50
|
-
const open = text[start]!
|
|
51
|
-
const close = open === '{' ? '}' : open === '[' ? ']' : ')'
|
|
52
|
-
let depth = 0
|
|
53
|
-
let inString: string | false = false
|
|
54
|
-
|
|
55
|
-
for (let i = start; i < text.length; i++) {
|
|
56
|
-
const ch = text[i]
|
|
57
|
-
|
|
58
|
-
if (inString) {
|
|
59
|
-
if (ch === '\\') {
|
|
60
|
-
i++
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
if (ch === inString) inString = false
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (ch === "'" || ch === '"' || ch === '`') {
|
|
68
|
-
inString = ch
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (ch === open) depth++
|
|
73
|
-
if (ch === close) {
|
|
74
|
-
depth--
|
|
75
|
-
if (depth === 0) return i
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return -1
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Extract the text between the outermost { } of defineConfig({ ... })
|
|
84
|
-
*/
|
|
85
|
-
export function extractConfigBody(content: string): string {
|
|
86
|
-
const match = content.match(/defineConfig\s*\(\s*\{/)
|
|
87
|
-
if (!match || match.index === undefined) return ''
|
|
88
|
-
|
|
89
|
-
const openBrace = content.indexOf('{', match.index + 'defineConfig'.length)
|
|
90
|
-
const closeBrace = findMatchingClose(content, openBrace)
|
|
91
|
-
if (closeBrace === -1) return ''
|
|
92
|
-
|
|
93
|
-
return content.slice(openBrace + 1, closeBrace)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Remove a top-level property from an object literal body text.
|
|
98
|
-
*/
|
|
99
|
-
export function removeProperty(body: string, propName: string): string {
|
|
100
|
-
const regex = new RegExp(`(\\n[ \\t]*)${propName}\\s*:\\s*`)
|
|
101
|
-
const match = regex.exec(body)
|
|
102
|
-
if (!match || match.index === undefined) return body
|
|
103
|
-
|
|
104
|
-
const propLineStart = match.index + 1 // skip the leading \n
|
|
105
|
-
const afterMatch = match.index + match[0].length
|
|
106
|
-
|
|
107
|
-
// Skip whitespace (not newlines) before the value
|
|
108
|
-
let i = afterMatch
|
|
109
|
-
while (i < body.length && (body[i] === ' ' || body[i] === '\t')) i++
|
|
110
|
-
|
|
111
|
-
let valueEnd: number
|
|
112
|
-
|
|
113
|
-
if (body[i] === '{' || body[i] === '[') {
|
|
114
|
-
valueEnd = findMatchingClose(body, i)
|
|
115
|
-
if (valueEnd === -1) return body
|
|
116
|
-
valueEnd++ // include closing brace/bracket
|
|
117
|
-
} else {
|
|
118
|
-
// Simple value (false, true, number, string, variable)
|
|
119
|
-
while (i < body.length && body[i] !== ',' && body[i] !== '\n') i++
|
|
120
|
-
valueEnd = i
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Skip trailing comma and whitespace up to newline
|
|
124
|
-
let end = valueEnd
|
|
125
|
-
while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
|
|
126
|
-
if (end < body.length && body[end] === ',') end++
|
|
127
|
-
while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
|
|
128
|
-
if (end < body.length && body[end] === '\n') end++
|
|
129
|
-
|
|
130
|
-
return body.slice(0, propLineStart) + body.slice(end)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
43
|
/**
|
|
134
44
|
* Prepend items into an existing array property (e.g. `integrations: [` or `plugins: [`).
|
|
135
45
|
* Mutates `lines` in place. Returns true if a merge happened.
|
|
@@ -205,14 +115,7 @@ export function transformConfig(content: string, disabled: Set<FeatureKey>): str
|
|
|
205
115
|
|
|
206
116
|
const allLines = [...bodyLines, ...newPropLines]
|
|
207
117
|
|
|
208
|
-
|
|
209
|
-
result += 'export default defineConfig({\n'
|
|
210
|
-
if (allLines.length > 0) {
|
|
211
|
-
result += allLines.join('\n') + '\n'
|
|
212
|
-
}
|
|
213
|
-
result += '})\n'
|
|
214
|
-
|
|
215
|
-
return result
|
|
118
|
+
return assembleConfig(imports, allLines.join('\n'))
|
|
216
119
|
}
|
|
217
120
|
|
|
218
121
|
export function transformPackageJson(
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ function printUsage() {
|
|
|
38
38
|
console.log(' build Run astro build with the Nua defaults')
|
|
39
39
|
console.log(' dev Run astro dev with the Nua defaults')
|
|
40
40
|
console.log(' preview Run astro preview with the Nua defaults')
|
|
41
|
+
console.log(' init Convert a standard Astro project to use Nua')
|
|
41
42
|
console.log(' clean Eject to a standard Astro project (remove @nuasite/* deps)')
|
|
42
43
|
console.log(' help Show this message')
|
|
43
44
|
console.log('\nAll Astro CLI options are supported.\n')
|
|
@@ -86,6 +87,15 @@ if (canProxyDirectly && command && ['build', 'dev', 'preview'].includes(command)
|
|
|
86
87
|
})
|
|
87
88
|
break
|
|
88
89
|
}
|
|
90
|
+
case 'init': {
|
|
91
|
+
const { init } = await import('./init')
|
|
92
|
+
await init({
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
dryRun: args.includes('--dry-run'),
|
|
95
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
96
|
+
})
|
|
97
|
+
break
|
|
98
|
+
}
|
|
89
99
|
case 'clean': {
|
|
90
100
|
const { clean } = await import('./clean')
|
|
91
101
|
await clean({
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { assembleConfig, extractConfigBody, findAstroConfig, findMatchingClose } from './utils'
|
|
4
|
+
import type { CommandOptions } from './utils'
|
|
5
|
+
|
|
6
|
+
export type InitOptions = CommandOptions
|
|
7
|
+
|
|
8
|
+
const NUA_PROVIDED_PACKAGES = [
|
|
9
|
+
'@astrojs/mdx',
|
|
10
|
+
'@astrojs/sitemap',
|
|
11
|
+
'@tailwindcss/vite',
|
|
12
|
+
'tailwindcss',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const NUA_MANAGED_PACKAGES = [
|
|
16
|
+
'@astrojs/mdx',
|
|
17
|
+
'@astrojs/sitemap',
|
|
18
|
+
'@tailwindcss/vite',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect which Nua-managed packages are explicitly imported in the config.
|
|
23
|
+
* Returns a map of package specifier → local import name.
|
|
24
|
+
*/
|
|
25
|
+
export function detectNuaManagedImports(content: string): Map<string, string> {
|
|
26
|
+
const managed = new Map<string, string>()
|
|
27
|
+
for (const pkg of NUA_MANAGED_PACKAGES) {
|
|
28
|
+
const regex = new RegExp(`import\\s+(\\w+)\\s+from\\s+['"]${pkg.replace('/', '\\/')}['"]`)
|
|
29
|
+
const match = regex.exec(content)
|
|
30
|
+
if (match) {
|
|
31
|
+
managed.set(pkg, match[1]!)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return managed
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Remove a function call (e.g. `mdx()` or `sitemap({ ... })`) from an array property.
|
|
39
|
+
*/
|
|
40
|
+
export function removeCallFromArray(body: string, arrayProp: string, callName: string): string {
|
|
41
|
+
const propRegex = new RegExp(`\\b${arrayProp}\\s*:\\s*\\[`)
|
|
42
|
+
const propMatch = propRegex.exec(body)
|
|
43
|
+
if (!propMatch || propMatch.index === undefined) return body
|
|
44
|
+
|
|
45
|
+
const arrayStart = body.indexOf('[', propMatch.index)
|
|
46
|
+
const arrayEnd = findMatchingClose(body, arrayStart)
|
|
47
|
+
if (arrayEnd === -1) return body
|
|
48
|
+
|
|
49
|
+
const arrayContent = body.slice(arrayStart + 1, arrayEnd)
|
|
50
|
+
|
|
51
|
+
const callRegex = new RegExp(`\\b${callName}\\s*\\(`)
|
|
52
|
+
const callMatch = callRegex.exec(arrayContent)
|
|
53
|
+
if (!callMatch || callMatch.index === undefined) return body
|
|
54
|
+
|
|
55
|
+
const parenStart = arrayContent.indexOf('(', callMatch.index)
|
|
56
|
+
const parenEnd = findMatchingClose(arrayContent, parenStart)
|
|
57
|
+
if (parenEnd === -1) return body
|
|
58
|
+
|
|
59
|
+
let removeStart = callMatch.index
|
|
60
|
+
let removeEnd = parenEnd + 1
|
|
61
|
+
|
|
62
|
+
let after = removeEnd
|
|
63
|
+
while (after < arrayContent.length && (arrayContent[after] === ' ' || arrayContent[after] === '\t' || arrayContent[after] === '\n')) {
|
|
64
|
+
after++
|
|
65
|
+
}
|
|
66
|
+
if (after < arrayContent.length && arrayContent[after] === ',') {
|
|
67
|
+
removeEnd = after + 1
|
|
68
|
+
while (
|
|
69
|
+
removeEnd < arrayContent.length
|
|
70
|
+
&& (arrayContent[removeEnd] === ' ' || arrayContent[removeEnd] === '\t' || arrayContent[removeEnd] === '\n')
|
|
71
|
+
) removeEnd++
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (removeEnd === parenEnd + 1) {
|
|
75
|
+
let before = removeStart - 1
|
|
76
|
+
while (before >= 0 && (arrayContent[before] === ' ' || arrayContent[before] === '\t' || arrayContent[before] === '\n')) before--
|
|
77
|
+
if (before >= 0 && arrayContent[before] === ',') {
|
|
78
|
+
removeStart = before
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const newArrayContent = arrayContent.slice(0, removeStart) + arrayContent.slice(removeEnd)
|
|
83
|
+
return body.slice(0, arrayStart + 1) + newArrayContent + body.slice(arrayEnd)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clean up empty structures left after removing managed integrations/plugins.
|
|
88
|
+
* Removes empty arrays/objects and the Nua-default `sourcemap: true`.
|
|
89
|
+
*/
|
|
90
|
+
export function cleanEmptyStructures(body: string): string {
|
|
91
|
+
body = body.replace(/\n[ \t]*sourcemap\s*:\s*true\s*,?[ \t]*/g, '\n')
|
|
92
|
+
body = body.replace(/\n[ \t]*build\s*:\s*\{[\s,]*\}\s*,?[ \t]*/g, '\n')
|
|
93
|
+
body = body.replace(/\n[ \t]*plugins\s*:\s*\[[\s,]*\]\s*,?[ \t]*/g, '\n')
|
|
94
|
+
body = body.replace(/\n[ \t]*integrations\s*:\s*\[[\s,]*\]\s*,?[ \t]*/g, '\n')
|
|
95
|
+
body = body.replace(/\n[ \t]*vite\s*:\s*\{[\s,]*\}\s*,?[ \t]*/g, '\n')
|
|
96
|
+
|
|
97
|
+
return body
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function transformConfig(content: string, managedImports: Map<string, string>): string {
|
|
101
|
+
const lines = content.split('\n')
|
|
102
|
+
const newImports: string[] = []
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (!/^\s*import\s/.test(line)) continue
|
|
106
|
+
|
|
107
|
+
if (line.includes('astro/config') && line.includes('defineConfig')) {
|
|
108
|
+
newImports.push(`import { defineConfig } from '@nuasite/nua/config'`)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isManagedImport = [...managedImports.keys()].some(pkg => line.includes(pkg))
|
|
113
|
+
if (isManagedImport) continue
|
|
114
|
+
|
|
115
|
+
newImports.push(line)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let body = extractConfigBody(content)
|
|
119
|
+
|
|
120
|
+
for (const [pkg, localName] of managedImports) {
|
|
121
|
+
if (pkg === '@tailwindcss/vite') {
|
|
122
|
+
body = removeCallFromArray(body, 'plugins', localName)
|
|
123
|
+
} else {
|
|
124
|
+
body = removeCallFromArray(body, 'integrations', localName)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
body = cleanEmptyStructures(body)
|
|
129
|
+
|
|
130
|
+
return assembleConfig(newImports, body)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveNuaVersion(): string {
|
|
134
|
+
try {
|
|
135
|
+
const cliPkgPath = new URL('../../package.json', import.meta.url)
|
|
136
|
+
const cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'))
|
|
137
|
+
const version: string = cliPkg.version
|
|
138
|
+
const [major, minor] = version.split('.')
|
|
139
|
+
return `^${major}.${minor}.0`
|
|
140
|
+
} catch {
|
|
141
|
+
return '^0.17.0'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function transformPackageJson(pkg: Record<string, any>, nuaVersion: string): Record<string, any> {
|
|
146
|
+
const result = structuredClone(pkg)
|
|
147
|
+
|
|
148
|
+
if (result.scripts) {
|
|
149
|
+
for (const [key, value] of Object.entries(result.scripts)) {
|
|
150
|
+
if (typeof value === 'string') {
|
|
151
|
+
result.scripts[key] = value
|
|
152
|
+
.replace(/\bastro build\b/g, 'nua build')
|
|
153
|
+
.replace(/\bastro dev\b/g, 'nua dev')
|
|
154
|
+
.replace(/\bastro preview\b/g, 'nua preview')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const field of ['dependencies', 'devDependencies'] as const) {
|
|
160
|
+
if (!result[field]) continue
|
|
161
|
+
for (const name of NUA_PROVIDED_PACKAGES) {
|
|
162
|
+
delete result[field][name]
|
|
163
|
+
}
|
|
164
|
+
if (Object.keys(result[field]).length === 0) {
|
|
165
|
+
delete result[field]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!result.dependencies) result.dependencies = {}
|
|
170
|
+
if (!result.dependencies['@nuasite/nua']) {
|
|
171
|
+
result.dependencies['@nuasite/nua'] = nuaVersion
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.dependencies = Object.fromEntries(
|
|
175
|
+
Object.entries(result.dependencies).sort(([a], [b]) => a.localeCompare(b)),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function init({ cwd = process.cwd(), dryRun = false, yes = false }: InitOptions = {}) {
|
|
182
|
+
const configPath = findAstroConfig(cwd)
|
|
183
|
+
if (!configPath) {
|
|
184
|
+
console.error('No Astro config file found.')
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const configContent = readFileSync(configPath, 'utf-8')
|
|
189
|
+
|
|
190
|
+
if (configContent.includes('@nuasite/nua')) {
|
|
191
|
+
console.log('This project already uses @nuasite/nua. Nothing to do.')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!configContent.includes('defineConfig')) {
|
|
196
|
+
console.error('Could not find defineConfig in Astro config.')
|
|
197
|
+
process.exit(1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pkgPath = join(cwd, 'package.json')
|
|
201
|
+
if (!existsSync(pkgPath)) {
|
|
202
|
+
console.error('No package.json found.')
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
206
|
+
|
|
207
|
+
const managedImports = detectNuaManagedImports(configContent)
|
|
208
|
+
const nuaVersion = resolveNuaVersion()
|
|
209
|
+
const configName = basename(configPath)
|
|
210
|
+
|
|
211
|
+
console.log('')
|
|
212
|
+
console.log('nua init \u2014 adopt the Nua toolchain')
|
|
213
|
+
console.log('')
|
|
214
|
+
console.log(` ${configName}`)
|
|
215
|
+
console.log(' - Replace astro/config with @nuasite/nua/config')
|
|
216
|
+
if (managedImports.size > 0) {
|
|
217
|
+
console.log(` - Remove Nua-managed imports: ${[...managedImports.keys()].join(', ')}`)
|
|
218
|
+
console.log(' - Remove managed integration/plugin calls')
|
|
219
|
+
}
|
|
220
|
+
console.log(' - Clean up empty config structures')
|
|
221
|
+
console.log('')
|
|
222
|
+
console.log(' package.json')
|
|
223
|
+
const removable = NUA_PROVIDED_PACKAGES.filter(name => pkg.dependencies?.[name] || pkg.devDependencies?.[name])
|
|
224
|
+
if (removable.length > 0) {
|
|
225
|
+
console.log(` - Remove Nua-provided deps: ${removable.join(', ')}`)
|
|
226
|
+
}
|
|
227
|
+
console.log(` - Add @nuasite/nua ${nuaVersion}`)
|
|
228
|
+
console.log(' - Update scripts: astro \u2192 nua')
|
|
229
|
+
|
|
230
|
+
if (dryRun) {
|
|
231
|
+
console.log('')
|
|
232
|
+
console.log(' (--dry-run: no changes made)')
|
|
233
|
+
console.log('')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!yes) {
|
|
238
|
+
console.log('')
|
|
239
|
+
const answer = prompt('Proceed? [y/N] ')
|
|
240
|
+
if (answer?.toLowerCase() !== 'y') {
|
|
241
|
+
console.log('Cancelled.')
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const newConfig = transformConfig(configContent, managedImports)
|
|
247
|
+
writeFileSync(configPath, newConfig)
|
|
248
|
+
console.log(` Updated ${configName}`)
|
|
249
|
+
|
|
250
|
+
const newPkg = transformPackageJson(pkg, nuaVersion)
|
|
251
|
+
writeFileSync(pkgPath, JSON.stringify(newPkg, null, '\t') + '\n')
|
|
252
|
+
console.log(' Updated package.json')
|
|
253
|
+
|
|
254
|
+
console.log('')
|
|
255
|
+
console.log('Next steps:')
|
|
256
|
+
console.log(' 1. bun install')
|
|
257
|
+
console.log(' 2. Review the updated config')
|
|
258
|
+
console.log(' 3. nua dev')
|
|
259
|
+
console.log('')
|
|
260
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
|
+
export interface CommandOptions {
|
|
5
|
+
cwd?: string
|
|
6
|
+
dryRun?: boolean
|
|
7
|
+
yes?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
const CONFIG_NAMES = [
|
|
5
11
|
'astro.config.ts',
|
|
6
12
|
'astro.config.mts',
|
|
@@ -15,3 +21,106 @@ export function findAstroConfig(cwd: string = process.cwd()): string | null {
|
|
|
15
21
|
}
|
|
16
22
|
return null
|
|
17
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Find the matching closing brace/bracket/paren, aware of string literals.
|
|
27
|
+
*/
|
|
28
|
+
export function findMatchingClose(text: string, start: number): number {
|
|
29
|
+
const open = text[start]!
|
|
30
|
+
const close = open === '{' ? '}' : open === '[' ? ']' : ')'
|
|
31
|
+
let depth = 0
|
|
32
|
+
let inString: string | false = false
|
|
33
|
+
|
|
34
|
+
for (let i = start; i < text.length; i++) {
|
|
35
|
+
const ch = text[i]
|
|
36
|
+
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (ch === '\\') {
|
|
39
|
+
i++
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (ch === inString) inString = false
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
47
|
+
inString = ch
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === open) depth++
|
|
52
|
+
if (ch === close) {
|
|
53
|
+
depth--
|
|
54
|
+
if (depth === 0) return i
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return -1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract the text between the outermost { } of defineConfig({ ... })
|
|
63
|
+
*/
|
|
64
|
+
export function extractConfigBody(content: string): string {
|
|
65
|
+
const match = content.match(/defineConfig\s*\(\s*\{/)
|
|
66
|
+
if (!match || match.index === undefined) return ''
|
|
67
|
+
|
|
68
|
+
const openBrace = content.indexOf('{', match.index + 'defineConfig'.length)
|
|
69
|
+
const closeBrace = findMatchingClose(content, openBrace)
|
|
70
|
+
if (closeBrace === -1) return ''
|
|
71
|
+
|
|
72
|
+
return content.slice(openBrace + 1, closeBrace)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Remove a top-level property from an object literal body text.
|
|
77
|
+
*/
|
|
78
|
+
export function removeProperty(body: string, propName: string): string {
|
|
79
|
+
const regex = new RegExp(`(\\n[ \\t]*)${propName}\\s*:\\s*`)
|
|
80
|
+
const match = regex.exec(body)
|
|
81
|
+
if (!match || match.index === undefined) return body
|
|
82
|
+
|
|
83
|
+
const propLineStart = match.index + 1 // skip the leading \n
|
|
84
|
+
const afterMatch = match.index + match[0].length
|
|
85
|
+
|
|
86
|
+
// Skip whitespace (not newlines) before the value
|
|
87
|
+
let i = afterMatch
|
|
88
|
+
while (i < body.length && (body[i] === ' ' || body[i] === '\t')) i++
|
|
89
|
+
|
|
90
|
+
let valueEnd: number
|
|
91
|
+
|
|
92
|
+
if (body[i] === '{' || body[i] === '[') {
|
|
93
|
+
valueEnd = findMatchingClose(body, i)
|
|
94
|
+
if (valueEnd === -1) return body
|
|
95
|
+
valueEnd++ // include closing brace/bracket
|
|
96
|
+
} else {
|
|
97
|
+
// Simple value (false, true, number, string, variable)
|
|
98
|
+
while (i < body.length && body[i] !== ',' && body[i] !== '\n') i++
|
|
99
|
+
valueEnd = i
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Skip trailing comma and whitespace up to newline
|
|
103
|
+
let end = valueEnd
|
|
104
|
+
while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
|
|
105
|
+
if (end < body.length && body[end] === ',') end++
|
|
106
|
+
while (end < body.length && (body[end] === ' ' || body[end] === '\t')) end++
|
|
107
|
+
if (end < body.length && body[end] === '\n') end++
|
|
108
|
+
|
|
109
|
+
return body.slice(0, propLineStart) + body.slice(end)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Assemble a complete config file from import lines and a body string.
|
|
114
|
+
*/
|
|
115
|
+
export function assembleConfig(imports: string[], body: string): string {
|
|
116
|
+
const bodyLines = body.split('\n').filter(line => line.trim() !== '')
|
|
117
|
+
|
|
118
|
+
let result = imports.join('\n') + '\n\n'
|
|
119
|
+
result += 'export default defineConfig({\n'
|
|
120
|
+
if (bodyLines.length > 0) {
|
|
121
|
+
result += bodyLines.join('\n') + '\n'
|
|
122
|
+
}
|
|
123
|
+
result += '})\n'
|
|
124
|
+
|
|
125
|
+
return result
|
|
126
|
+
}
|