@nasl/cli 0.3.0 → 0.3.2

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 CHANGED
@@ -76,6 +76,20 @@ nasl check "src/app.logics.*.ts"
76
76
  nasl check "src/*.tsx"
77
77
  ```
78
78
 
79
+ ### 检查 plan 文档中的 NaturalTS(nasl-doc / nasld)
80
+
81
+ 用于检查 plan 目录下 markdown 里 `naturalts` 代码块的语法(与 `nasl check` 面向 `src` 的 NASL 源码检查不同)。
82
+
83
+ 安装 `@nasl/cli` 后,可使用 **`nasl-doc`** 或 **`nasld`**(同一命令的短名):
84
+
85
+ ```bash
86
+ nasl-doc check path/to/plan.md
87
+ # 或
88
+ nasld check path/to/plan.md
89
+ ```
90
+
91
+ 支持的文档类型包括:`enums.md`、`entity-*.md`、`structure-*.md`、`logic-*.md`、`view-*.md` 等(详见命令帮助:`nasl-doc check --help`)。
92
+
79
93
  ### 依赖分析(nasl dep)
80
94
 
81
95
  建议指定入口文件,否则会扫描整个 src 目录,输出量较大。
package/dist/bin/nasl.mjs CHANGED
@@ -26524,7 +26524,7 @@ function setProxy(options, configProxy, location) {
26524
26524
  options.headers['Proxy-Authorization'] = 'Basic ' + base64;
26525
26525
  }
26526
26526
 
26527
- options.headers.host = options.hostname + (options.port ? ':' + options.port : '');
26527
+ options.headers.host = options.hostname + (options.port ? ':' + options.port : options.protocol === 'https:' ? ':443' : '');
26528
26528
  const proxyHost = proxy.hostname || proxy.host;
26529
26529
  options.hostname = proxyHost;
26530
26530
  // Replace 'host' since options is not a URL object
@@ -26941,7 +26941,7 @@ var httpAdapter = isHttpAdapterSupported && function httpAdapter(config) {
26941
26941
  } else {
26942
26942
  options.hostname = parsed.hostname.startsWith("[") ? parsed.hostname.slice(1, -1) : parsed.hostname;
26943
26943
  options.port = parsed.port;
26944
- setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : parsed.protocol === 'https:' ? '443' : '') + options.path);
26944
+ setProxy(options, config.proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path);
26945
26945
  }
26946
26946
 
26947
26947
  let transport;
@@ -29331,7 +29331,7 @@ function validateExtensionsFile(file, errors) {
29331
29331
  /**
29332
29332
  * 验证普通文件(枚举、实体、数据结构、逻辑、页面)
29333
29333
  */
29334
- function validateNormalFile(file, nameFromPath, namespace, errors) {
29334
+ function validateNormalFile(file, nameFromPath, namespace, errors, options) {
29335
29335
  const matchArr = Array.from(file.content.matchAll(/^(export\s+)?(declare\s+)?(function|class|interface)\s+(\w+)/gm));
29336
29336
  if (matchArr.length === 0)
29337
29337
  errors.push(`${file.path} 必须有一个函数或类,错误代码:${file.content}`);
@@ -29343,14 +29343,14 @@ function validateNormalFile(file, nameFromPath, namespace, errors) {
29343
29343
  if (!isExport)
29344
29344
  errors.push(`${file.path} 必须使用 export,错误代码:${match[0]}`);
29345
29345
  if (name !== nameFromPath)
29346
- errors.push(`${file.path} 的函数或类名必须与文件名一致,错误代码:${match[0]}`);
29346
+ errors.push(`${file.path} 的函数或类名必须与${options?.isDocCheck ? ' path 属性' : '文件名'}一致,错误代码:${match[0]}`);
29347
29347
  if (/\.(entities|enums|structures)/.test(namespace) && type !== 'class')
29348
29348
  errors.push(`${file.path} 实体、数据结构和枚举只能使用 class 定义,错误代码:${match[0]}`);
29349
29349
  if (/\.(logics|views)/.test(namespace) && type !== 'function')
29350
29350
  errors.push(`${file.path} 逻辑和页面只能使用 function 定义,错误代码:${match[0]}`);
29351
29351
  }
29352
29352
  }
29353
- function composeToString(files) {
29353
+ function composeToString(files, options) {
29354
29354
  files.sort((a, b) => sorter(a.path, b.path));
29355
29355
  const errors = [];
29356
29356
  let currentLine = 1;
@@ -29365,7 +29365,7 @@ function composeToString(files) {
29365
29365
  const isExtensionsFile = arr[0] === 'extensions';
29366
29366
  const isThemeCss = ext === 'css' && nameFromPath === 'theme';
29367
29367
  const isSpecialFile = isVariablesFile || isExtensionsFile || isThemeCss;
29368
- // 特殊文件的 namespace 包含文件名,普通文件不包含
29368
+ // 特殊文件的 namespace ${包含文件名,普通文件不包含
29369
29369
  const namespace = isSpecialFile ? [...arr, nameFromPath].join('.') : arr.join('.');
29370
29370
  let content = file.content;
29371
29371
  if (['ts', 'tsx'].includes(ext)) {
@@ -29376,7 +29376,7 @@ function composeToString(files) {
29376
29376
  validateExtensionsFile(file, errors);
29377
29377
  }
29378
29378
  else {
29379
- validateNormalFile(file, nameFromPath, namespace, errors);
29379
+ validateNormalFile(file, nameFromPath, namespace, errors, options);
29380
29380
  }
29381
29381
  }
29382
29382
  else if (isThemeCss) {
@@ -39035,6 +39035,170 @@ async function transformJson2FilesApi(json, options) {
39035
39035
  return data.result;
39036
39036
  }
39037
39037
 
39038
+ // src/codeBlock.ts
39039
+ function extractCodeBlocks(markdown) {
39040
+ const codeBlocks = [];
39041
+ const lines = markdown.split("\n");
39042
+ let inCodeBlock = false;
39043
+ let codeBlockStart = 0;
39044
+ let currLang = "";
39045
+ let currLangExtra = "";
39046
+ let currCode = [];
39047
+ for (let i = 0; i < lines.length; i++) {
39048
+ const line = lines[i];
39049
+ if (line.startsWith("```")) {
39050
+ if (!inCodeBlock) {
39051
+ inCodeBlock = true;
39052
+ codeBlockStart = i + 1;
39053
+ const langPart = line.slice(3).trim();
39054
+ const spaceIndex = langPart.indexOf(" ");
39055
+ let potentialLang = "";
39056
+ let potentialExtra = "";
39057
+ if (spaceIndex > 0) {
39058
+ potentialLang = langPart.slice(0, spaceIndex);
39059
+ potentialExtra = langPart.slice(spaceIndex + 1).trim();
39060
+ } else {
39061
+ potentialLang = langPart;
39062
+ potentialExtra = "";
39063
+ }
39064
+ const isLikelyLanguage = potentialLang && !potentialLang.includes("=") && !potentialLang.startsWith("/") && !potentialLang.startsWith(".") && potentialLang !== "";
39065
+ if (isLikelyLanguage) {
39066
+ currLang = potentialLang;
39067
+ currLangExtra = potentialExtra;
39068
+ } else {
39069
+ currLang = "text";
39070
+ currLangExtra = langPart;
39071
+ }
39072
+ currCode = [];
39073
+ } else {
39074
+ inCodeBlock = false;
39075
+ codeBlocks.push({
39076
+ lang: currLang,
39077
+ langExtra: currLangExtra || void 0,
39078
+ code: currCode.join("\n"),
39079
+ startLine: codeBlockStart,
39080
+ endLine: i
39081
+ });
39082
+ currLang = "";
39083
+ currLangExtra = "";
39084
+ currCode = [];
39085
+ }
39086
+ } else if (inCodeBlock) {
39087
+ currCode.push(line);
39088
+ }
39089
+ }
39090
+ return codeBlocks;
39091
+ }
39092
+
39093
+ const DOC_PATTERNS = [
39094
+ /^enums\.md$/,
39095
+ /^entity-.+\.md$/,
39096
+ /^structure-.+\.md$/,
39097
+ /^logic-.+\.md$/,
39098
+ /^view-.+\.md$/,
39099
+ ];
39100
+ // 文档类型前缀 → NASL 文件类型名
39101
+ const DOC_TO_NASL_TYPE = {
39102
+ enums: 'enums',
39103
+ entity: 'entities',
39104
+ structure: 'structures',
39105
+ logic: 'logics',
39106
+ view: 'views',
39107
+ };
39108
+ /**
39109
+ * 从 tsx FileInfo 中解析出所有祖先 view,生成空壳 stub
39110
+ * 例如 path=a.views.dashboard.views.knowledgeAccess.views.accessBrowse.tsx
39111
+ * 会生成 dashboard 和 knowledgeAccess 的 stub
39112
+ */
39113
+ function buildViewStubs(files) {
39114
+ const existingPaths = new Set(files.map((f) => f.path));
39115
+ const stubs = [];
39116
+ for (const file of files) {
39117
+ if (!file.path.endsWith('.tsx'))
39118
+ continue;
39119
+ const parts = file.path.split('.').slice(0, -2);
39120
+ for (let i = 0; i < parts.length - 1; i++) {
39121
+ if (parts[i] === 'views') {
39122
+ const viewName = parts[i + 1];
39123
+ if (!viewName)
39124
+ continue;
39125
+ const stubPath = [...parts.slice(0, i + 2), 'tsx'].join('.');
39126
+ if (!existingPaths.has(stubPath)) {
39127
+ existingPaths.add(stubPath);
39128
+ stubs.push({
39129
+ path: stubPath,
39130
+ content: `$View({\n title: "",\n auth: false,\n isIndex: false,\n});\nexport function ${viewName}() {\n return ( <ElRouterView /> );\n}`,
39131
+ });
39132
+ }
39133
+ }
39134
+ }
39135
+ }
39136
+ return stubs;
39137
+ }
39138
+ /**
39139
+ * 从 plan markdown 文件中提取并组装 fullNaturalTS,返回字符串或错误信息
39140
+ */
39141
+ function extractFullNaturalTS(filePath, logger = defaultLogger) {
39142
+ // 1. 验证路径模式(只检查文件名)
39143
+ const basename = path$1.basename(filePath);
39144
+ if (!DOC_PATTERNS.some((p) => p.test(basename))) {
39145
+ logger.error(`路径不匹配任何支持的文档类型:${basename}\n` +
39146
+ `支持的类型:enums.md, entity-*.md, structure-*.md, logic-*.md, view-*.md`);
39147
+ return { success: false };
39148
+ }
39149
+ // 2. 读取文件
39150
+ if (!libExports.existsSync(filePath)) {
39151
+ logger.error(`文件不存在:${filePath}`);
39152
+ return { success: false };
39153
+ }
39154
+ const content = libExports.readFileSync(filePath, 'utf-8');
39155
+ // 3. 提取 naturalts 代码块,转换为 FileInfo[]
39156
+ const blocks = extractCodeBlocks(content).filter((b) => b.lang === 'naturalts');
39157
+ if (blocks.length === 0) {
39158
+ // logger.warn('未发现 naturalts 代码块');
39159
+ return { fullNaturalTS: '' };
39160
+ }
39161
+ const isViewDoc = /^view-.+\.md$/.test(basename);
39162
+ const expectedExt = isViewDoc ? '.tsx' : '.ts';
39163
+ const docTypePrefix = basename.split(/[-.]/)[0] ?? '';
39164
+ const naslTypeName = DOC_TO_NASL_TYPE[docTypePrefix];
39165
+ const expectedPattern = NASL_FILE_TYPES.find((t) => t.name === naslTypeName)?.fileNamePattern;
39166
+ const errors = [];
39167
+ const files = blocks.map((block) => {
39168
+ const match = block.langExtra?.match(/path="([^"]+)"/);
39169
+ if (!match) {
39170
+ errors.push(`代码块缺少 path 属性(第 ${block.startLine} 行)`);
39171
+ return null;
39172
+ }
39173
+ const blockPath = match[1];
39174
+ if (!blockPath.endsWith(expectedExt)) {
39175
+ errors.push(`代码块 path 扩展名不正确,应为 ${expectedExt}(第 ${block.startLine} 行):${blockPath}`);
39176
+ return null;
39177
+ }
39178
+ if (!isKnownFileType(blockPath)) {
39179
+ const hint = expectedPattern ? `\n正确格式应匹配:${expectedPattern}` : '';
39180
+ errors.push(`代码块 path 不是合法的 NASL 文件类型(第 ${block.startLine} 行):${blockPath}${hint}`);
39181
+ return null;
39182
+ }
39183
+ const fileInfo = { path: blockPath, content: block.code };
39184
+ return fileInfo;
39185
+ }).filter((f) => f !== null);
39186
+ if (errors.length > 0) {
39187
+ logger.error(`在 ${filePath} 检测到以下错误:\n` + errors.join('\n'));
39188
+ return { success: false };
39189
+ }
39190
+ // 4. 补充父级 view stub(tsx 文件需要祖先页面定义)
39191
+ const stubs = buildViewStubs(files);
39192
+ // 5. 复用 composeToString 组装(包含排序和 namespace 构建)
39193
+ try {
39194
+ return { fullNaturalTS: composeToString([...files, ...stubs], { isDocCheck: true }) };
39195
+ }
39196
+ catch (err) {
39197
+ logger.error(`在 ${filePath} 检测到以下错误:\n` + err.message);
39198
+ return { success: false };
39199
+ }
39200
+ }
39201
+
39038
39202
  const transformFns = {
39039
39203
  /**
39040
39204
  * 将 src 中的文件组合成 fullNaturalTS
@@ -39061,6 +39225,33 @@ const transformFns = {
39061
39225
  writeFileWithLog(outputPath, fullNaturalTS, logger);
39062
39226
  logger.success(`文件已输出到: ${outputPath}`);
39063
39227
  },
39228
+ /**
39229
+ * 将 plan markdown 文档中的 naturalts 代码块组合成 fullNaturalTS 文件
39230
+ */
39231
+ async doc2full(entry, options) {
39232
+ const logger = options?.logger || defaultLogger;
39233
+ if (!entry) {
39234
+ logger.error('请指定 plan 文档路径');
39235
+ logger.exit(1);
39236
+ return;
39237
+ }
39238
+ const extracted = extractFullNaturalTS(entry, logger);
39239
+ if ('success' in extracted) {
39240
+ logger.exit(1);
39241
+ return;
39242
+ }
39243
+ if (!extracted.fullNaturalTS) {
39244
+ logger.warn('未发现 naturalts 代码块,跳过输出');
39245
+ return;
39246
+ }
39247
+ // 确定输出路径
39248
+ const projectRoot = getProjectRoot();
39249
+ const outputPath = options?.output
39250
+ ? path$1.resolve(projectRoot, options.output)
39251
+ : path$1.join(projectRoot, './full-natural.ts');
39252
+ writeFileWithLog(outputPath, extracted.fullNaturalTS, logger);
39253
+ logger.success(`文件已输出到: ${outputPath}`);
39254
+ },
39064
39255
  /**
39065
39256
  * 将 JSON 文件转换成 src 的 files
39066
39257
  * TODO: 待实现
@@ -39193,7 +39384,7 @@ async function installByJSON(json, options) {
39193
39384
  logger.success(`成功写入 ${writtenCount} 个依赖文件到 ${config.srcDir} 目录`);
39194
39385
  }
39195
39386
 
39196
- var version = "0.3.0";
39387
+ var version = "0.3.2";
39197
39388
  var pkg = {
39198
39389
  version: version};
39199
39390
 
@@ -39355,12 +39546,12 @@ program
39355
39546
  });
39356
39547
  program
39357
39548
  .command('transform <transformType> [entry]')
39358
- .description('转换文件格式\n transformType: files2full (将 src 文件组合成 fullNaturalTS), json2files (将 JSON 转换为文件), files2json (将 src 文件转换为 JSON)')
39549
+ .description('转换文件格式\n transformType: files2full (将 src 文件组合成 fullNaturalTS), json2files (将 JSON 转换为文件), files2json (将 src 文件转换为 JSON), doc2full (将 plan markdown 文档组合成 fullNaturalTS)')
39359
39550
  .option('-o, --output <outputPath>', '指定输出路径')
39360
39551
  .option('-v, --verbose', '显示详细信息,如依赖分析树')
39361
39552
  .action(async (transformType, entry, options) => {
39362
39553
  try {
39363
- const validTypes = ['files2full', 'json2files', 'files2json'];
39554
+ const validTypes = ['files2full', 'json2files', 'files2json', 'doc2full'];
39364
39555
  if (!validTypes.includes(transformType)) {
39365
39556
  defaultLogger.error(`无效的转换类型: ${transformType}。支持的类型: ${validTypes.join(', ')}`);
39366
39557
  process.exit(1);