@neuxnet/neux-cli 0.2.3 → 0.2.5

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/src/core/init.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { DiminaCliError } from './errors.js'
5
5
  import { pathExists, resolvePath, writeJson } from './fs.js'
6
- import { getCliVersion } from './package-info.js'
6
+ import { getCliPackageInfo } from './package-info.js'
7
7
  import { saveCliKey } from '../providers/service-config.js'
8
8
  import { promptLines, shouldPrompt } from './prompts.js'
9
9
 
@@ -12,7 +12,6 @@ const DEFAULT_LIB_VERSION = '3.3.3'
12
12
  export const DEFAULT_SERVER_URL = 'https://demo-c.paas.superapp.neuvision.cn'
13
13
  const CLI_PACKAGE_NAME = '@neuxnet/neux-cli'
14
14
  const TEMPLATE_ASSET_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../assets/init')
15
- const DEFAULT_DOCS_BASE_URL = `${DEFAULT_SERVER_URL}/miniappdoc`
16
15
 
17
16
  export async function initProject(options = {}) {
18
17
  const targetArg = options._?.[0] || options.project || '.'
@@ -25,8 +24,14 @@ export async function initProject(options = {}) {
25
24
  const appId = options.appId || prompts.appId || DEFAULT_APP_ID
26
25
  const serverUrl = options.serverUrl || prompts.serverUrl || DEFAULT_SERVER_URL
27
26
  const cliKey = options.key || prompts.cliKey
28
- const cliVersion = await getCliVersion()
29
- const files = createTemplateFiles({ appId, projectName, serverUrl, cliVersion })
27
+ const cliPackage = await getCliPackageInfo()
28
+ const files = createTemplateFiles({
29
+ appId,
30
+ projectName,
31
+ serverUrl,
32
+ cliVersion: cliPackage.version,
33
+ typesPackage: cliPackage.typesPackage,
34
+ })
30
35
 
31
36
  for (const file of files) {
32
37
  const filePath = path.join(projectPath, file.path)
@@ -36,9 +41,7 @@ export async function initProject(options = {}) {
36
41
  await fs.mkdir(path.dirname(filePath), { recursive: true })
37
42
  const sourcePath = path.join(TEMPLATE_ASSET_DIR, file.source)
38
43
  if (file.source === 'types/neux-api.d.ts') {
39
- const docsBaseUrl = `${String(serverUrl || DEFAULT_SERVER_URL).replace(/\/+$/, '')}/miniappdoc`
40
- const content = await fs.readFile(sourcePath, 'utf8')
41
- await fs.writeFile(filePath, content.replaceAll(DEFAULT_DOCS_BASE_URL, docsBaseUrl), 'utf8')
44
+ await fs.writeFile(filePath, `/// <reference types="${file.typesPackage.name}" />\n`, 'utf8')
42
45
  }
43
46
  else await fs.copyFile(sourcePath, filePath)
44
47
  }
@@ -136,13 +139,45 @@ async function mergeJsonFile(filePath, generated) {
136
139
  ...(existing['files.associations'] || {}),
137
140
  }
138
141
  }
142
+ if (generated?.['json.schemas'] || existing?.['json.schemas']) {
143
+ const generatedSchemas = Array.isArray(generated?.['json.schemas']) ? generated['json.schemas'] : []
144
+ const existingSchemas = Array.isArray(existing?.['json.schemas']) ? existing['json.schemas'] : []
145
+ const keys = new Set()
146
+ merged['json.schemas'] = [...generatedSchemas, ...existingSchemas].filter(schema => {
147
+ const key = `${schema?.url || ''}|${JSON.stringify(schema?.fileMatch || [])}`
148
+ if (keys.has(key)) return false
149
+ keys.add(key)
150
+ return true
151
+ })
152
+ }
153
+ if (generated?.['html.customData'] || existing?.['html.customData']) {
154
+ merged['html.customData'] = [
155
+ ...new Set([
156
+ ...(Array.isArray(generated?.['html.customData']) ? generated['html.customData'] : []),
157
+ ...(Array.isArray(existing?.['html.customData']) ? existing['html.customData'] : []),
158
+ ]),
159
+ ]
160
+ }
139
161
  if (filePath.endsWith('neux.code-snippets')) {
140
162
  Object.assign(merged, generated || {}, existing || {})
141
163
  }
142
164
  await writeJson(filePath, merged)
143
165
  }
144
166
 
145
- function createTemplateFiles({ appId, projectName, serverUrl = DEFAULT_SERVER_URL, cliVersion }) {
167
+ function createTemplateFiles({
168
+ appId,
169
+ projectName,
170
+ serverUrl = DEFAULT_SERVER_URL,
171
+ cliVersion,
172
+ typesPackage,
173
+ }) {
174
+ if (!typesPackage?.name || !typesPackage?.versionRange) {
175
+ throw new DiminaCliError(
176
+ 'NEUX_CLI_TYPES_PACKAGE_METADATA_MISSING',
177
+ 'The CLI package does not declare a compatible miniapp types package.',
178
+ )
179
+ }
180
+
146
181
  return [
147
182
  {
148
183
  path: 'project.config.json',
@@ -198,6 +233,7 @@ function createTemplateFiles({ appId, projectName, serverUrl = DEFAULT_SERVER_UR
198
233
  },
199
234
  devDependencies: {
200
235
  [CLI_PACKAGE_NAME]: `^${cliVersion}`,
236
+ [typesPackage.name]: typesPackage.versionRange,
201
237
  },
202
238
  },
203
239
  },
@@ -260,8 +296,21 @@ function createTemplateFiles({ appId, projectName, serverUrl = DEFAULT_SERVER_UR
260
296
  '*.nxml': 'html',
261
297
  '*.nxss': 'css',
262
298
  },
299
+ 'json.schemas': createJsonSchemaSettings(),
300
+ 'html.customData': ['./.vscode/neux-html-data.json'],
263
301
  },
264
302
  },
303
+ {
304
+ path: '.vscode/neux-html-data.json',
305
+ kind: 'asset',
306
+ source: 'neux-html-data.json',
307
+ },
308
+ {
309
+ path: '.vscode/neux-components.code-snippets',
310
+ kind: 'asset',
311
+ source: 'neux-components.code-snippets',
312
+ },
313
+ ...createJsonSchemaFiles(),
265
314
  {
266
315
  path: '.vscode/neux.code-snippets',
267
316
  kind: 'merge-json',
@@ -276,11 +325,17 @@ function createTemplateFiles({ appId, projectName, serverUrl = DEFAULT_SERVER_UR
276
325
  checkJs: false,
277
326
  noEmit: true,
278
327
  target: 'ES2020',
328
+ types: [typesPackage.name],
279
329
  },
280
330
  include: ['**/*.js', '**/*.ts', 'types/**/*.d.ts'],
281
331
  },
282
332
  },
283
- { path: 'types/neux-api.d.ts', kind: 'asset', source: 'types/neux-api.d.ts' },
333
+ {
334
+ path: 'types/neux-api.d.ts',
335
+ kind: 'asset',
336
+ source: 'types/neux-api.d.ts',
337
+ typesPackage,
338
+ },
284
339
  { path: 'assets/tabbar/home.png', kind: 'asset', source: 'tabbar/home.png' },
285
340
  { path: 'assets/tabbar/home-active.png', kind: 'asset', source: 'tabbar/home-active.png' },
286
341
  { path: 'assets/tabbar/list.png', kind: 'asset', source: 'tabbar/list.png' },
@@ -522,6 +577,30 @@ function createTemplateFiles({ appId, projectName, serverUrl = DEFAULT_SERVER_UR
522
577
  ]
523
578
  }
524
579
 
580
+ function createJsonSchemaSettings() {
581
+ return [
582
+ { fileMatch: ['/app.json'], url: './.vscode/neux-json-schemas/app.schema.json' },
583
+ { fileMatch: ['/pages/**/*.json', '/components/**/*.json'], url: './.vscode/neux-json-schemas/page.schema.json' },
584
+ { fileMatch: ['/project.config.json'], url: './.vscode/neux-json-schemas/project-config.schema.json' },
585
+ { fileMatch: ['/project.private.config.json'], url: './.vscode/neux-json-schemas/project-private-config.schema.json' },
586
+ { fileMatch: ['/neux.config.json'], url: './.vscode/neux-json-schemas/neux-config.schema.json' },
587
+ ]
588
+ }
589
+
590
+ function createJsonSchemaFiles() {
591
+ return [
592
+ 'app.schema.json',
593
+ 'page.schema.json',
594
+ 'project-config.schema.json',
595
+ 'project-private-config.schema.json',
596
+ 'neux-config.schema.json',
597
+ ].map(name => ({
598
+ path: `.vscode/neux-json-schemas/${name}`,
599
+ kind: 'asset',
600
+ source: `schemas/${name}`,
601
+ }))
602
+ }
603
+
525
604
  function escapeJsString(value) {
526
605
  return String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")
527
606
  }
@@ -759,13 +838,6 @@ function createNeuxCodeSnippets() {
759
838
  body: ['<view nx:if="{{${1:visible}}}">', ' ${0}', '</view>'],
760
839
  description: 'Render a view conditionally',
761
840
  },
762
- 'Tap Button': {
763
- prefix: 'nxbutton',
764
- scope: 'html',
765
- include: ['**/*.nxml'],
766
- body: ['<button bindtap="${1:handleTap}">${2:Button}</button>$0'],
767
- description: 'Insert a button with a tap handler',
768
- },
769
841
  'Page View': {
770
842
  prefix: 'nxview',
771
843
  scope: 'html',
@@ -15,6 +15,7 @@ export async function getCliPackageInfo() {
15
15
  packageName: packageJson.name || brand.packageName,
16
16
  version: packageJson.version,
17
17
  packageRoot,
18
+ typesPackage: packageJson.neux?.typesPackage || null,
18
19
  }
19
20
  }
20
21
 
@@ -27,9 +27,16 @@ export async function inspectProject(options = {}) {
27
27
  const appName = options.name || effectiveProjectConfig.projectname || effectiveProjectConfig.projectName || `App ${appId || 'unknown'}`
28
28
  const entryPath = options.entryPath || appConfig.entryPagePath || pages[0] || 'pages/index'
29
29
  const hasPrivateConfig = await pathExists(projectPrivateConfigPath)
30
+ const diagnostics = await validateProjectConfiguration({
31
+ projectPath,
32
+ miniprogramRoot,
33
+ sourceRoot,
34
+ appConfig,
35
+ pages,
36
+ })
30
37
 
31
38
  return {
32
- ok: true,
39
+ ok: diagnostics.every(item => item.severity !== 'error'),
33
40
  command: 'inspect',
34
41
  projectPath,
35
42
  sourceRoot,
@@ -48,7 +55,137 @@ export async function inspectProject(options = {}) {
48
55
  condition: effectiveProjectConfig.condition || {},
49
56
  neuxCli: effectiveProjectConfig.neuxCli || {},
50
57
  cliConfig,
58
+ diagnostics,
59
+ }
60
+ }
61
+
62
+ export async function validateProjectConfiguration({ projectPath, miniprogramRoot = '', sourceRoot, appConfig = {}, pages = [] } = {}) {
63
+ const diagnostics = []
64
+ projectPath ||= sourceRoot
65
+ const add = (severity, code, message, pathName, details = {}) => {
66
+ diagnostics.push({ severity, code, message, path: pathName, ...details })
67
+ }
68
+ const pageSet = new Set(pages.map(normalizeProjectPath))
69
+ const entryPagePath = normalizeProjectPath(appConfig.entryPagePath || pages[0])
70
+
71
+ if (!sourceRoot) {
72
+ add('error', 'NEUX_SOURCE_ROOT_INVALID', 'A source root is required for project validation', 'project.config.json')
73
+ return diagnostics
74
+ }
75
+ if (miniprogramRoot && path.isAbsolute(String(miniprogramRoot))) {
76
+ add('error', 'NEUX_MINIPROGRAM_ROOT_INVALID', 'miniprogramRoot must be a relative path', 'project.config.json', { value: miniprogramRoot })
77
+ }
78
+ const normalizedRoot = normalizeProjectPath(miniprogramRoot)
79
+ if (normalizedRoot && !isWithinProjectRoot(projectPath, sourceRoot)) {
80
+ add('error', 'NEUX_MINIPROGRAM_ROOT_OUTSIDE_PROJECT', 'miniprogramRoot resolves outside the project directory', 'project.config.json', { value: miniprogramRoot })
81
+ }
82
+ if (normalizedRoot && !(await pathExists(sourceRoot))) {
83
+ add('error', 'NEUX_MINIPROGRAM_ROOT_NOT_FOUND', `miniprogramRoot does not exist: ${miniprogramRoot}`, 'project.config.json', { value: miniprogramRoot })
84
+ }
85
+ if (appConfig.entryPagePath && !pageSet.has(entryPagePath)) {
86
+ add('error', 'NEUX_ENTRY_PAGE_NOT_DECLARED', `Entry page is not declared in pages: ${appConfig.entryPagePath}`, 'app.json', { value: appConfig.entryPagePath })
87
+ }
88
+
89
+ for (const page of pages) {
90
+ const normalized = normalizeProjectPath(page)
91
+ if (!(await hasAnyProjectFile(sourceRoot, normalized, ['.json', '.nxml', '.nxss', '.js', '.ts', '.wxml', '.wxss']))) {
92
+ add('error', 'NEUX_PAGE_NOT_FOUND', `Page does not exist: ${page}`, 'app.json', { value: page })
93
+ continue
94
+ }
95
+ const pageConfigPath = path.join(sourceRoot, `${normalized}.json`)
96
+ const pageConfig = await readJson(pageConfigPath, {})
97
+ await validateUsingComponents(pageConfig.usingComponents, path.dirname(pageConfigPath), sourceRoot, `${page}.json`, add)
51
98
  }
99
+
100
+ const tabItems = Array.isArray(appConfig.tabBar?.list) ? appConfig.tabBar.list : []
101
+ if (appConfig.tabBar && !Array.isArray(appConfig.tabBar.list)) {
102
+ add('error', 'NEUX_TABBAR_LIST_INVALID', 'tabBar.list must be an array', 'app.json')
103
+ }
104
+ if (tabItems.length > 5) add('error', 'NEUX_TABBAR_TOO_MANY_ITEMS', 'tabBar.list cannot contain more than 5 items', 'app.json', { count: tabItems.length })
105
+ if (appConfig.tabBar && tabItems.length === 0) add('error', 'NEUX_TABBAR_LIST_EMPTY', 'tabBar.list must contain at least one item', 'app.json')
106
+ for (const [index, item] of tabItems.entries()) {
107
+ const pagePath = normalizeProjectPath(item?.pagePath)
108
+ if (!pagePath) {
109
+ add('error', 'NEUX_TABBAR_PAGE_INVALID', `tabBar.list[${index}].pagePath must be a non-empty string`, 'app.json')
110
+ } else if (!pageSet.has(pagePath)) {
111
+ add('error', 'NEUX_TABBAR_PAGE_NOT_DECLARED', `TabBar page is not declared in pages: ${item.pagePath}`, 'app.json', { value: item.pagePath })
112
+ }
113
+ for (const field of ['iconPath', 'selectedIconPath']) {
114
+ if (item?.[field] && !(await pathExists(path.resolve(sourceRoot, item[field])))) {
115
+ add('error', 'NEUX_ASSET_NOT_FOUND', `TabBar asset does not exist: ${item[field]}`, 'app.json', { value: item[field], field })
116
+ }
117
+ }
118
+ }
119
+
120
+ await validateUsingComponents(appConfig.usingComponents, sourceRoot, sourceRoot, 'app.json', add)
121
+ const subPackages = appConfig.subPackages || appConfig.subpackages
122
+ if (Array.isArray(subPackages)) {
123
+ const roots = new Set()
124
+ for (const [index, subPackage] of subPackages.entries()) {
125
+ const root = normalizeProjectPath(subPackage?.root)
126
+ if (!root) {
127
+ add('error', 'NEUX_SUBPACKAGE_ROOT_INVALID', `subPackages[${index}].root must be a non-empty string`, 'app.json')
128
+ continue
129
+ }
130
+ if (roots.has(root)) add('error', 'NEUX_SUBPACKAGE_ROOT_DUPLICATE', `Duplicate subpackage root: ${root}`, 'app.json', { value: root })
131
+ roots.add(root)
132
+ for (const page of Array.isArray(subPackage.pages) ? subPackage.pages : []) {
133
+ const pagePath = normalizeProjectPath(path.posix.join(root, page))
134
+ if (pageSet.has(pagePath)) {
135
+ add('error', 'NEUX_PAGE_IN_MULTIPLE_PACKAGES', `Page is declared in both the main package and a subpackage: ${pagePath}`, 'app.json', { value: pagePath })
136
+ }
137
+ if (!(await hasAnyProjectFile(sourceRoot, pagePath, ['.json', '.nxml', '.nxss', '.js', '.ts', '.wxml', '.wxss']))) {
138
+ add('error', 'NEUX_SUBPACKAGE_PAGE_NOT_FOUND', `Subpackage page does not exist: ${pagePath}`, 'app.json', { value: pagePath })
139
+ }
140
+ }
141
+ }
142
+ const rootEntries = [...roots]
143
+ for (let i = 0; i < rootEntries.length; i += 1) {
144
+ for (let j = i + 1; j < rootEntries.length; j += 1) {
145
+ if (rootEntries[i].startsWith(`${rootEntries[j]}/`) || rootEntries[j].startsWith(`${rootEntries[i]}/`)) {
146
+ add('error', 'NEUX_SUBPACKAGE_ROOT_OVERLAP', `Subpackage roots overlap: ${rootEntries[i]} and ${rootEntries[j]}`, 'app.json')
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return diagnostics
152
+ }
153
+
154
+ async function validateUsingComponents(usingComponents, ownerDir, sourceRoot, configPath, add) {
155
+ if (!usingComponents || typeof usingComponents !== 'object' || Array.isArray(usingComponents)) return
156
+ for (const [name, target] of Object.entries(usingComponents)) {
157
+ if (typeof target !== 'string' || !target.trim()) {
158
+ add('error', 'NEUX_COMPONENT_PATH_INVALID', `Component path must be a non-empty string: ${name}`, configPath, { value: target })
159
+ continue
160
+ }
161
+ if (target.startsWith('plugin://') || target.startsWith('npm:') || (!target.startsWith('.') && !target.startsWith('/'))) continue
162
+ const normalized = target.startsWith('/') ? target.slice(1) : path.relative(sourceRoot, path.resolve(ownerDir, target))
163
+ if (!(await hasAnyProjectFile(sourceRoot, normalizeProjectPath(normalized), ['.json', '.nxml', '.nxss', '.js', '.ts', '.wxml', '.wxss']))) {
164
+ add('error', 'NEUX_COMPONENT_NOT_FOUND', `Component does not exist: ${target}`, configPath, { value: target, component: name })
165
+ }
166
+ }
167
+ }
168
+
169
+ async function hasAnyProjectFile(sourceRoot, basePath, extensions) {
170
+ const candidates = [basePath]
171
+ const basename = path.posix.basename(basePath)
172
+ candidates.push(path.posix.join(basePath, basename))
173
+ for (const candidate of candidates) {
174
+ for (const extension of extensions) {
175
+ if (await pathExists(path.join(sourceRoot, `${candidate}${extension}`))) return true
176
+ }
177
+ }
178
+ return false
179
+ }
180
+
181
+ function normalizeProjectPath(value) {
182
+ return String(value || '').replaceAll('\\', '/').replace(/^\/+|\/+$/g, '')
183
+ }
184
+
185
+ function isWithinProjectRoot(projectPath, sourceRoot) {
186
+ const root = path.resolve(projectPath || sourceRoot)
187
+ const candidate = path.resolve(sourceRoot)
188
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`)
52
189
  }
53
190
 
54
191
  function mergeProjectConfig(projectConfig = {}, privateConfig = {}) {
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { buildProject, buildStyleOnlyProject, buildViewOnlyProject, buildLogicOnlyProject } from './core/compiler.js'
2
2
  export { DEFAULT_SERVER_URL, initProject } from './core/init.js'
3
- export { inspectProject } from './core/project.js'
3
+ export { inspectProject, validateProjectConfiguration } from './core/project.js'
4
4
  export { packProject } from './core/pack.js'
5
5
  export { startPreview } from './core/preview-server.js'
6
6
  export { startDev } from './core/dev.js'