@nuasite/cli 0.65.3 → 0.66.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 +20 -0
- package/dist/index.js +35719 -35363
- package/dist/types/check-code.d.ts +16 -2
- package/dist/types/check-code.d.ts.map +1 -1
- package/dist/types/check.d.ts +5 -0
- package/dist/types/check.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/check-code.ts +96 -5
- package/src/check.ts +42 -3
- package/src/index.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuasite/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@astrojs/compiler": "^4.0.0",
|
|
27
27
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
28
|
-
"@nuasite/cms": "0.
|
|
29
|
-
"@nuasite/cms-core": "0.
|
|
28
|
+
"@nuasite/cms": "0.66.0",
|
|
29
|
+
"@nuasite/cms-core": "0.66.0",
|
|
30
|
+
"tailwindcss": "4.2.2"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/bun": "1.3.11"
|
package/src/check-code.ts
CHANGED
|
@@ -8,17 +8,29 @@
|
|
|
8
8
|
* This is a *syntax* check, not a type check. A wrong prop type or a missing export still needs
|
|
9
9
|
* `astro check` or a build. What it does catch is the error that stops a build dead before a single
|
|
10
10
|
* page renders — an unescaped quote inside a Czech string being the one that prompted it.
|
|
11
|
+
*
|
|
12
|
+
* Stylesheets are parsed the way the build parses them. A Nua site builds with Tailwind's Vite
|
|
13
|
+
* plugin, which runs every stylesheet through Tailwind's CSS parser, Tailwind or not, and that
|
|
14
|
+
* parser refuses what a browser quietly repairs: a rule cut off before its closing brace failed a
|
|
15
|
+
* whole site's build while the editor preview, which concatenates the files, only lost its styles.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { transform } from '@astrojs/compiler'
|
|
14
19
|
import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'
|
|
15
20
|
import type { CheckFinding } from '@nuasite/cms-core'
|
|
16
21
|
import { Glob } from 'bun'
|
|
22
|
+
import { createRequire } from 'node:module'
|
|
17
23
|
import path from 'node:path'
|
|
24
|
+
import { compile as bundledCompile } from 'tailwindcss'
|
|
25
|
+
|
|
26
|
+
type Compile = typeof bundledCompile
|
|
18
27
|
|
|
19
28
|
/** Directories that are never the project's own source. */
|
|
20
29
|
const IGNORED = ['node_modules', 'dist', '.astro', '.git', '.output', '.vercel', '.netlify']
|
|
21
30
|
|
|
31
|
+
/** Copied to the output as they are: nothing parses them, so nothing in them can fail a build. */
|
|
32
|
+
const UNPROCESSED_STYLESHEETS = ['public']
|
|
33
|
+
|
|
22
34
|
const isIgnored = (relative: string): boolean =>
|
|
23
35
|
// Declaration files are types only — nothing compiles them, and the TS loader rejects them.
|
|
24
36
|
/\.d\.[mc]?ts$/.test(relative) || relative.split(path.sep).some(segment => IGNORED.includes(segment))
|
|
@@ -103,31 +115,110 @@ function checkScriptFile(relative: string, source: string, loader: 'ts' | 'tsx')
|
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The parser's own syntax errors. Compiling a stylesheet alone also reports what only the whole
|
|
120
|
+
* project could settle (`@apply` of a utility its theme defines, a `@reference` it needs), and those
|
|
121
|
+
* are left to the build.
|
|
122
|
+
*/
|
|
123
|
+
const CSS_SYNTAX_ERROR = /^(Missing closing|Missing opening|Unterminated string|Invalid declaration)/
|
|
124
|
+
|
|
125
|
+
/** A Tailwind `CssSyntaxError`: its message is `<from>:<line>:<column>: <what>`, its `loc` the offsets into the source. */
|
|
126
|
+
function cssSyntaxError(error: unknown): { message: string; offset: number | null } | null {
|
|
127
|
+
if (!(error instanceof Error)) return null
|
|
128
|
+
const message = error.message.replace(/^.*?:\d+:\d+: /, '')
|
|
129
|
+
if (!CSS_SYNTAX_ERROR.test(message)) return null
|
|
130
|
+
const loc = 'loc' in error && Array.isArray(error.loc) ? error.loc : null
|
|
131
|
+
const offset = loc && typeof loc[1] === 'number' ? loc[1] : null
|
|
132
|
+
return { message, offset }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 1-based line and column of an offset, the way editors and build output count them. */
|
|
136
|
+
function lineAndColumn(source: string, offset: number): { line: number; column: number } {
|
|
137
|
+
const before = source.slice(0, offset)
|
|
138
|
+
const line = before.split('\n').length
|
|
139
|
+
return { line, column: offset - before.lastIndexOf('\n') }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The site's own Tailwind when it has one, else the copy this CLI ships. Its parser's strictness moved between
|
|
144
|
+
* versions (4.0 accepts `color red;`, 4.1 refuses it), and the check has to refuse what the site's build refuses.
|
|
145
|
+
*/
|
|
146
|
+
async function compileFor(root: string): Promise<Compile> {
|
|
147
|
+
try {
|
|
148
|
+
const module: unknown = await import(createRequire(path.join(root, 'package.json')).resolve('tailwindcss'))
|
|
149
|
+
if (typeof module === 'object' && module !== null && 'compile' in module && typeof module.compile === 'function') {
|
|
150
|
+
return module.compile as Compile
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Not installed in the site, or not loadable from here: the bundled parser is the next best judge.
|
|
154
|
+
}
|
|
155
|
+
return bundledCompile
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function checkCssFile(compile: Compile, absolute: string, relative: string, source: string): Promise<CheckFinding | null> {
|
|
159
|
+
try {
|
|
160
|
+
// Imports and plugins resolve to nothing: each stylesheet is parsed on its own, and loading the
|
|
161
|
+
// project's would only add errors that are not syntax.
|
|
162
|
+
await compile(source, {
|
|
163
|
+
base: path.dirname(absolute),
|
|
164
|
+
from: relative,
|
|
165
|
+
loadStylesheet: async (id, base) => ({ path: id, base, content: '' }),
|
|
166
|
+
loadModule: async (id, base) => ({ path: id, base, module: () => {} }),
|
|
167
|
+
})
|
|
168
|
+
return null
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const syntax = cssSyntaxError(error)
|
|
171
|
+
if (!syntax) return null
|
|
172
|
+
return {
|
|
173
|
+
severity: 'error',
|
|
174
|
+
code: 'code/css-syntax',
|
|
175
|
+
file: relative,
|
|
176
|
+
...(syntax.offset === null ? {} : lineAndColumn(source, syntax.offset)),
|
|
177
|
+
message: syntax.message,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
106
182
|
export interface CodeCheckResult {
|
|
107
183
|
findings: CheckFinding[]
|
|
108
184
|
files: number
|
|
185
|
+
/** The files parsed, relative to the root: what `--fail-on` can vouch for. */
|
|
186
|
+
paths: string[]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface CodeCheckOptions {
|
|
190
|
+
/**
|
|
191
|
+
* May the check load the project's own packages? Its `tailwindcss` is the site's real parser, but loading it runs
|
|
192
|
+
* the project's installed code, which `--no-live` promises not to do; the bundled parser stands in then.
|
|
193
|
+
*/
|
|
194
|
+
projectCode: boolean
|
|
109
195
|
}
|
|
110
196
|
|
|
111
|
-
/** Parse every `.astro`/`.ts`/`.tsx` under `root
|
|
112
|
-
export async function checkCode(root: string): Promise<CodeCheckResult> {
|
|
197
|
+
/** Parse every `.astro`/`.ts`/`.tsx`/`.css` under `root`; order is by path. */
|
|
198
|
+
export async function checkCode(root: string, { projectCode }: CodeCheckOptions = { projectCode: false }): Promise<CodeCheckResult> {
|
|
113
199
|
const findings: CheckFinding[] = []
|
|
114
200
|
let files = 0
|
|
115
201
|
|
|
116
202
|
const paths: string[] = []
|
|
117
|
-
for await (const relative of new Glob('**/*.{astro,ts,tsx,mts,cts}').scan({ cwd: root })) {
|
|
118
|
-
if (
|
|
203
|
+
for await (const relative of new Glob('**/*.{astro,ts,tsx,mts,cts,css}').scan({ cwd: root })) {
|
|
204
|
+
if (isIgnored(relative)) continue
|
|
205
|
+
if (relative.endsWith('.css') && UNPROCESSED_STYLESHEETS.includes(relative.split(path.sep)[0]!)) continue
|
|
206
|
+
paths.push(relative)
|
|
119
207
|
}
|
|
120
208
|
paths.sort()
|
|
121
209
|
|
|
210
|
+
const compile = projectCode && paths.some(relative => relative.endsWith('.css')) ? await compileFor(root) : bundledCompile
|
|
122
211
|
for (const relative of paths) {
|
|
123
212
|
files++
|
|
124
213
|
const absolute = path.join(root, relative)
|
|
125
214
|
const source = await Bun.file(absolute).text()
|
|
126
215
|
const finding = relative.endsWith('.astro')
|
|
127
216
|
? await checkAstroFile(absolute, relative, source)
|
|
217
|
+
: relative.endsWith('.css')
|
|
218
|
+
? await checkCssFile(compile, absolute, relative, source)
|
|
128
219
|
: checkScriptFile(relative, source, relative.endsWith('.tsx') ? 'tsx' : 'ts')
|
|
129
220
|
if (finding) findings.push(finding)
|
|
130
221
|
}
|
|
131
222
|
|
|
132
|
-
return { findings, files }
|
|
223
|
+
return { findings, files, paths }
|
|
133
224
|
}
|
package/src/check.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface CheckOptions {
|
|
|
13
13
|
contentOnly: boolean
|
|
14
14
|
/** Validate against the project's real schemas, which means importing and running its content config. */
|
|
15
15
|
live: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Fail only on errors in these files, relative to `cwd`; every finding is still reported. For a commit
|
|
18
|
+
* that must not add a broken file but should not be blocked by one it did not touch.
|
|
19
|
+
*/
|
|
20
|
+
failOn: string[] | null
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -24,6 +29,7 @@ export interface CheckOptions {
|
|
|
24
29
|
export function parseCheckArgs(args: string[], cwd: string): CheckOptions {
|
|
25
30
|
const contentOnly = args.includes('--content-only')
|
|
26
31
|
return {
|
|
32
|
+
failOn: parseFailOn(args),
|
|
27
33
|
cwd,
|
|
28
34
|
json: args.includes('--json'),
|
|
29
35
|
strict: args.includes('--strict'),
|
|
@@ -33,6 +39,15 @@ export function parseCheckArgs(args: string[], cwd: string): CheckOptions {
|
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
/** `--fail-on a.css,b.astro` or `--fail-on=a.css,b.astro`, as normalized relative paths; null without the flag. */
|
|
43
|
+
function parseFailOn(args: string[]): string[] | null {
|
|
44
|
+
const index = args.findIndex(arg => arg === '--fail-on' || arg.startsWith('--fail-on='))
|
|
45
|
+
if (index === -1) return null
|
|
46
|
+
const flag = args[index]!
|
|
47
|
+
const value = flag === '--fail-on' ? args[index + 1] ?? '' : flag.slice('--fail-on='.length)
|
|
48
|
+
return value.split(',').map(file => file.trim()).filter(Boolean).map(file => path.normalize(file))
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
const ASTRO_CONFIG_NAMES = ['astro.config.ts', 'astro.config.mts', 'astro.config.mjs', 'astro.config.js']
|
|
37
52
|
|
|
38
53
|
const hasAstroConfig = (dir: string): boolean => ASTRO_CONFIG_NAMES.some(name => existsSync(path.join(dir, name)))
|
|
@@ -102,12 +117,13 @@ export async function check(options: CheckOptions): Promise<number> {
|
|
|
102
117
|
const reports = await Promise.all(roots.map(async root => {
|
|
103
118
|
const absolute = path.join(options.cwd, root)
|
|
104
119
|
const content = prefixed(await checkContent(createNodeFs(absolute), schemas ? { schemas } : {}), root)
|
|
105
|
-
if (options.contentOnly) return { ...content, files: 0 }
|
|
106
|
-
const code = await checkCode(absolute)
|
|
120
|
+
if (options.contentOnly) return { ...content, files: 0, paths: [] }
|
|
121
|
+
const code = await checkCode(absolute, { projectCode: options.live })
|
|
107
122
|
return {
|
|
108
123
|
...content,
|
|
109
124
|
findings: [...content.findings, ...code.findings.map(finding => ({ ...finding, file: path.join(root, finding.file) }))],
|
|
110
125
|
files: code.files,
|
|
126
|
+
paths: code.paths.map(file => path.join(root, file)),
|
|
111
127
|
}
|
|
112
128
|
}))
|
|
113
129
|
|
|
@@ -120,10 +136,21 @@ export async function check(options: CheckOptions): Promise<number> {
|
|
|
120
136
|
|
|
121
137
|
const errors = report.findings.filter(finding => finding.severity === 'error').length
|
|
122
138
|
const warnings = report.findings.length - errors
|
|
139
|
+
// An agent may list a file by its absolute path; findings are relative to the cwd.
|
|
140
|
+
const failOn = options.failOn === null ? null : new Set(options.failOn.map(file => path.relative(options.cwd, path.resolve(options.cwd, file))))
|
|
141
|
+
// Under --strict a warning in a listed file fails as well, as it would without --fail-on.
|
|
142
|
+
const failing = failOn === null
|
|
143
|
+
? errors
|
|
144
|
+
: report.findings.filter(finding => (finding.severity === 'error' || options.strict) && failOn.has(path.normalize(finding.file))).length
|
|
145
|
+
// A listed file the syntax pass did not read, e.g. a `.js`, one in `public/` or outside every Astro project:
|
|
146
|
+
// nothing here parsed it, so it is named rather than counted, and the summary cannot read as a pass for it.
|
|
147
|
+
const parsed = new Set(reports.flatMap(one => one.paths))
|
|
148
|
+
const unchecked = failOn === null ? [] : [...failOn].filter(file => !parsed.has(file))
|
|
123
149
|
|
|
124
150
|
if (options.json) {
|
|
125
151
|
// The reason belongs inside the payload: a stray line would break anything parsing this.
|
|
126
|
-
|
|
152
|
+
const failOnErrors = failOn === null ? {} : { failOnErrors: failing, failOnUnchecked: unchecked }
|
|
153
|
+
console.log(JSON.stringify({ ...report, roots, liveSchemas: 'schemas' in live ? 'ran' : live.skipped, ...failOnErrors }, null, 2))
|
|
127
154
|
} else {
|
|
128
155
|
const formatted = formatCheckReport(report)
|
|
129
156
|
if (formatted) console.log(formatted + '\n')
|
|
@@ -133,7 +160,19 @@ export async function check(options: CheckOptions): Promise<number> {
|
|
|
133
160
|
console.log(errors === 0 && warnings === 0 ? `${counts} — no problems found` : `${counts} — ${errors} error(s), ${warnings} warning(s)`)
|
|
134
161
|
// A check that did not run must not read as a pass.
|
|
135
162
|
if ('skipped' in live) console.log(`Live schema rules did not run: ${live.skipped}`)
|
|
163
|
+
if (failOn !== null) {
|
|
164
|
+
const covered = failOn.size - unchecked.length
|
|
165
|
+
console.log(
|
|
166
|
+
failing === 0
|
|
167
|
+
? `No errors in the ${covered} file(s) given to --fail-on that were checked.`
|
|
168
|
+
: `${failing} problem(s) in the files given to --fail-on. Fix them before going on.`,
|
|
169
|
+
)
|
|
170
|
+
if (unchecked.length > 0) {
|
|
171
|
+
console.log(`Not parsed by this run (a content entry is still checked with its collection): ${unchecked.join(', ')}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
136
174
|
}
|
|
137
175
|
|
|
176
|
+
if (failOn !== null) return failing > 0 ? 1 : 0
|
|
138
177
|
return errors > 0 || (options.strict && warnings > 0) ? 1 : 0
|
|
139
178
|
}
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,9 @@ function printUsage() {
|
|
|
25
25
|
console.log(' build Run astro build with the Nua defaults')
|
|
26
26
|
console.log(' dev Run astro dev with the Nua defaults')
|
|
27
27
|
console.log(' preview Run astro preview with the Nua defaults')
|
|
28
|
-
console.log(' check Validate collections and parse the source (no build). --json, --strict, --content-only, --no-live')
|
|
28
|
+
console.log(' check Validate collections and parse the source (no build). --json, --strict, --content-only, --no-live,')
|
|
29
|
+
console.log(' --fail-on <file,...> (fail only on errors in these files). --no-live also keeps the')
|
|
30
|
+
console.log(" project's own tailwindcss unloaded; stylesheets are then parsed with the bundled one")
|
|
29
31
|
console.log(' init Convert a standard Astro project to use Nua')
|
|
30
32
|
console.log(' clean Eject to a standard Astro project (remove @nuasite/* deps)')
|
|
31
33
|
console.log(' migrate <target> Run a content migration. Targets: astro-image')
|