初始化仓库

初始化仓库
This commit is contained in:
2026-09-09 13:19:09 +08:00
parent 484d3877aa
commit d36050cff7
3289 changed files with 499918 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');
// 找到 assets 里的 css 文件
const assets = fs.readdirSync('templates/assets');
const cssFile = assets.find((f) => f.endsWith('.css') && f !== '.gitkeep');
const css = fs.readFileSync(path.join('templates/assets', cssFile), 'utf8');
// 简单 CSS beautifier(带缩进)
function beautify(css) {
let out = '';
let indent = 0;
let i = 0;
while (i < css.length) {
const ch = css[i];
if (ch === '{') {
out += ' {\n';
indent++;
} else if (ch === '}') {
indent = Math.max(0, indent - 1);
out += '\n' + ' '.repeat(indent) + '}\n';
} else if (ch === ';') {
out += ';\n' + ' '.repeat(indent);
} else if (ch === '\n' || ch === '\r') {
// 跳过原始换行
} else {
out += ch;
}
i++;
}
return out;
}
const beautified = beautify(css);
fs.writeFileSync('src/css/main.css', beautified);
console.log('✓ CSS 反压缩完成 -> src/css/main.css (' + beautified.length + ' 字符)');
+21
View File
@@ -0,0 +1,21 @@
const fs = require('fs');
const files = [
'src/post.html',
'src/index.html',
'src/header-menu.html',
'src/category.html',
'src/user-menu.html'
];
let total = 0;
for (const f of files) {
let c = fs.readFileSync(f, 'utf8');
const before = c;
c = c.replace(/([\w.]+) != ([\w]+)/g, 'not ($1 == $2)');
if (c !== before) {
fs.writeFileSync(f, c);
const diff = (before.match(/!=/g) || []).length;
console.log('✓', f, '替换', diff, '处');
total += diff;
}
}
console.log('共替换', total, '处');
+18
View File
@@ -0,0 +1,18 @@
const fs = require('fs');
let c = fs.readFileSync('src/index.html', 'utf8');
// 1. modules 兜底为空列表
c = c.replace(
' <div class="homepage">\n <th:block th:each="mod : ${theme.config.homepage.modules}">',
' <div class="homepage">\n <th:block th:with="modules = ${theme.config.homepage?.modules ?: #lists.toList()}">\n <th:block th:each="mod : ${modules}">'
);
c = c.replace(
' </th:block>\n\n </th:block>\n\n <section class="section browse"',
' </th:block>\n\n </th:block>\n </th:block>\n\n <section class="section browse"'
);
// 2. 所有 th:each="x : ${mod.xxx}" 加 ?: #lists.toList() 兜底
c = c.replace(/th:each="([^"]+) : \$\{mod\.(\w+)\}"/g, 'th:each="$1 : ${mod.$2 ?: #lists.toList()}"');
fs.writeFileSync('src/index.html', c);
console.log('done');
+29
View File
@@ -0,0 +1,29 @@
const fs = require('fs');
const path = require('path');
const dir = 'templates';
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.html'));
for (const f of files) {
const p = path.join(dir, f);
let c = fs.readFileSync(p, 'utf8');
const before = c;
// 1. theme.config.X. → theme.config.X?. (分组级安全导航,防止分组为 null 时 NPE)
c = c.replace(/theme\.config\.([a-z_]+)\./g, 'theme.config.$1?.');
// 2. nav_use_guide / nav_dev_guide 的 th:href 兜底,避免 @{null}
c = c.replace(
/\$\{theme\.config\.nav\?\.nav_use_guide\}/g,
"${theme.config.nav?.nav_use_guide ?: '/'}"
);
c = c.replace(
/\$\{theme\.config\.nav\?\.nav_dev_guide\}/g,
"${theme.config.nav?.nav_dev_guide ?: '/'}"
);
if (c !== before) {
fs.writeFileSync(p, c);
console.log('✓', f);
}
}
console.log('done');
+88
View File
@@ -0,0 +1,88 @@
const fs = require('fs');
const path = require('path');
const ROOT = process.cwd();
const SKILL = '/Users/huanghui/.codebuddy/skills/halo-theme-dev/assets/theme-vite';
// 1. 创建目录
for (const d of ['src', 'src/partials', 'src/css', 'src/js']) {
fs.mkdirSync(path.join(ROOT, d), { recursive: true });
}
// 2. 复制构建配置
fs.copyFileSync(path.join(SKILL, 'package.json'), path.join(ROOT, 'package.json'));
fs.copyFileSync(path.join(SKILL, 'vite.config.ts'), path.join(ROOT, 'vite.config.ts'));
console.log('✓ 已复制 package.json / vite.config.ts');
// 3. 从 templates/index.html 提取 layout 骨架
const idx = fs.readFileSync(path.join(ROOT, 'templates/index.html'), 'utf8');
const header = idx.match(/<header class="site-header">[\s\S]*?<\/header>/)[0];
const footer = idx.match(/<footer class="site-footer">[\s\S]*?<\/footer>/)[0];
const layout = `<!doctype html>
<html
lang="zh-CN"
xmlns:th="https://www.thymeleaf.org"
th:lang="\${#locale.toLanguageTag}"
class="color-scheme-auto"
data-color-scheme="auto"
>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<slot name="head">
<title th:text="\${site.title}">源蝶工作室</title>
</slot>
<script type="module" src="./js/main.ts"></script>
</head>
<body
th:style="'--primary-color:' + (\${theme.config.style?.primary_color} ?: '#2563eb') + ';--radius:' + (\${theme.config.style?.border_radius} ?: '12px')"
th:with="brandName = \${theme.config.brand?.brand_name ?: site.title}"
>
${header}
<main class="site-main">
<slot />
</main>
${footer}
</body>
</html>
`;
fs.writeFileSync(path.join(ROOT, 'src/partials/layout.html'), layout);
console.log('✓ 已生成 src/partials/layout.html');
// 4. 反推各页面(include + template + main 内容)
const pages = [
'index', 'post', 'category', 'tag', 'page',
'archives', 'author', 'categories', 'tags'
];
for (const name of pages) {
const tpl = fs.readFileSync(path.join(ROOT, `templates/${name}.html`), 'utf8');
const titleM = tpl.match(/<title[^>]*>[\s\S]*?<\/title>/);
const mainM = tpl.match(/<main class="site-main">([\s\S]*?)<\/main>/);
if (!mainM) {
console.log('⚠ 跳过(无 main:', name);
continue;
}
const title = titleM ? titleM[0] : `<title th:text="\${site.title}"></title>`;
const content = mainM[1].replace(/\n$/, '');
const out = `<include src="layout.html">
<template name="head">
${title}
</template>
${content}
</include>
`;
fs.writeFileSync(path.join(ROOT, `src/${name}.html`), out);
console.log('✓ 反推 src/' + name + '.html');
}
// 5. 复制 fragment 文件(不含 layout,直接复制)
for (const f of ['header-menu.html', 'user-menu.html', 'sidebar.html']) {
fs.copyFileSync(path.join(ROOT, `templates/${f}`), path.join(ROOT, `src/${f}`));
console.log('✓ 复制 src/' + f);
}
console.log('\n全部 html 反推完成');
BIN
View File
Binary file not shown.
+262
View File
@@ -0,0 +1,262 @@
const fs = require('fs');
let c = fs.readFileSync('settings.yaml', 'utf8');
const block = ` # ============ 首页模块(拖拽排序)============
- group: homepage
label: 首页
formSchema:
- $formkit: array
name: modules
label: 首页模块
help: 拖拽调整模块顺序,每个模块设置类型后填写对应字段
sortable: true
addLabel: 添加模块
value:
- type: hero-carousel
title: Hero
hc_title: "源蝶工作室 文档中心"
hc_subtitle: "源蝶工作室官方文档站,提供产品使用指南、开发者文档与解决方案。"
hc_primary_text: 快速开始
hc_primary_url: "/categories/quick-start"
hc_primary_color: ""
hc_secondary_text: 浏览文档
hc_secondary_url: "/categories"
hc_install_options:
- label: Docker
url: "/categories/install-docker"
- label: 1Panel
url: "/categories/install-1panel"
- label: 宝塔面板
url: "/categories/install-baota"
- label: 更多方式
url: "/categories/install"
hc_carousel_enabled: false
hc_carousel_speed: 5000
hc_carousel_show_dots: true
hc_carousel_images: []
- type: quick-start
title: 快速开始
qs_layout: default
qs_more_text: 查看使用指南
qs_more_url: "/categories/use-guide"
qs_cards:
- title: 初始化站点
subtitle: 安装之后
description: "完成首次访问、创建管理员账号和基础设置。"
url: "/categories/init"
primary: true
- title: 发布第一篇文章
subtitle: 熟悉内容编辑
description: "熟悉内容编辑和发布流程。"
url: "/categories/first-post"
- title: 探索应用市场
subtitle: 主题与插件
description: "发现丰富的主题、插件以扩展站点能力。"
url: "/categories/marketplace"
itemLabels:
- type: text
label: $value.title
children:
- $formkit: select
name: type
label: 模块类型
value: hero-carousel
options:
- label: 轮播图模块(Hero)
value: hero-carousel
- label: 快速开始模块
value: quick-start
- label: 卡片模块
value: cards
- label: Docsme 适配
value: plugin-docsme
- label: MiniDocs 适配
value: plugin-minidocs
- $formkit: text
name: title
label: 模块标题
value: ""
- $formkit: text
name: hc_title
label: "Hero 主标题(仅 hero-carousel"
value: ""
- $formkit: textarea
name: hc_subtitle
label: "Hero 副标题"
value: ""
- $formkit: text
name: hc_primary_text
label: 主按钮文字
value: 快速开始
- $formkit: text
name: hc_primary_url
label: 主按钮链接
value: ""
- $formkit: color
name: hc_primary_color
label: 主按钮颜色(留空用主题色)
value: ""
- $formkit: text
name: hc_secondary_text
label: 次按钮文字
value: 浏览文档
- $formkit: text
name: hc_secondary_url
label: 次按钮链接
value: ""
- $formkit: array
name: hc_install_options
label: 安装方式标签
value: []
itemLabels:
- type: text
label: $value.label
children:
- $formkit: text
name: label
label: 名称
value: ""
- $formkit: text
name: url
label: 链接
value: ""
- $formkit: switch
name: hc_carousel_enabled
label: 启用轮播图
value: false
- $formkit: number
name: hc_carousel_speed
label: 轮播速度(毫秒)
value: 5000
- $formkit: switch
name: hc_carousel_show_dots
label: 显示导航点
value: true
- $formkit: array
name: hc_carousel_images
label: 轮播图图片组
value: []
itemLabels:
- type: attachment
label: $value.src
children:
- $formkit: attachment
name: src
label: 图片
value: ""
- $formkit: text
name: alt
label: 描述
value: ""
- $formkit: text
name: href
label: 跳转链接
value: ""
- $formkit: select
name: qs_layout
label: "快速开始布局(仅 quick-start"
value: default
options:
- label: 默认三项
value: default
- label: 彩色三项
value: colorful
- label: 标题列表
value: titled-list
- $formkit: text
name: qs_more_text
label: 标题右侧按钮文字
value: 查看使用指南
- $formkit: text
name: qs_more_url
label: 标题右侧按钮链接
value: ""
- $formkit: array
name: qs_cards
label: 快速开始卡片
value: []
itemLabels:
- type: text
label: $value.title
children:
- $formkit: text
name: title
label: 标题
value: ""
- $formkit: text
name: subtitle
label: 副标题
value: ""
- $formkit: textarea
name: description
label: 描述
value: ""
- $formkit: text
name: url
label: 链接
value: ""
- $formkit: switch
name: primary
label: 设为主推卡片
value: false
- $formkit: select
name: cards_layout
label: "卡片布局(仅 cards"
value: type1
options:
- label: 样式1(背景+标题+描述)
value: type1
- label: 样式2(图标+标题+列表)
value: type2
- label: 样式3(简洁图标+标题)
value: type3
- $formkit: array
name: cards_items
label: 卡片项
value: []
itemLabels:
- type: text
label: $value.title
children:
- $formkit: attachment
name: cover
label: 封面图
value: ""
- $formkit: text
name: title
label: 标题
value: ""
- $formkit: textarea
name: description
label: 描述
value: ""
- $formkit: text
name: url
label: 链接
value: ""
- $formkit: switch
name: show_description
label: 显示描述
value: true
- $formkit: select
name: plugin_layout
label: 插件模块布局
value: cards
options:
- label: 卡片网格
value: cards
- label: 卡片+文档列表
value: cards-list
- label: 简洁卡片
value: simple
- $formkit: text
name: plugin_collection
label: 插件集合标识
value: ""
`;
c = c.replace(' # ============ 首页设置 ============\n - group: home', block + ' # ============ 首页设置(兼容旧版)============\n - group: home');
if (c.indexOf('group: homepage') === -1) { console.log('未匹配'); process.exit(1); }
fs.writeFileSync('settings.yaml', c);
const Y = require('./node_modules/.pnpm/yaml@2.9.0/node_modules/yaml');
const s = Y.parse(fs.readFileSync('settings.yaml','utf8'));
console.log('✓ 语法正确,分组:', s.spec.forms.map(x=>x.group).join(', '));
BIN
View File
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
import org.thymeleaf.standard.expression.*;
import java.lang.reflect.*;
public class TestEach {
public static void main(String[] args) throws Exception {
String[] exprs = {
"mod : ${modules}",
"x : ${modules}",
"m : ${modules}",
"card : ${modules}",
"mod : ${items}",
"module : ${modules}",
"mod2 : ${modules}",
};
Method m = EachUtils.class.getDeclaredMethod("internalParseEach", String.class);
m.setAccessible(true);
for (String e : exprs) {
try {
Object each = m.invoke(null, e);
System.out.println("OK : " + e + " -> " + each);
} catch (InvocationTargetException ex) {
System.out.println("FAIL : " + e + " -> " + ex.getCause());
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/@halo-dev+theme-package-cli@1.1.1/node_modules/@halo-dev/theme-package-cli/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/@halo-dev+theme-package-cli@1.1.1/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/@halo-dev+theme-package-cli@1.1.1/node_modules/@halo-dev/theme-package-cli/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/@halo-dev+theme-package-cli@1.1.1/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@halo-dev/theme-package-cli/index.js" "$@"
else
exec node "$basedir/../@halo-dev/theme-package-cli/index.js" "$@"
fi
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules/vite/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules/vite/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi
Generated Vendored
+448
View File
@@ -0,0 +1,448 @@
{
"hoistedDependencies": {
"archiver@7.0.1": {
"archiver": "private"
},
"commander@13.1.0": {
"commander": "private"
},
"fs-extra@11.4.0": {
"fs-extra": "private"
},
"js-yaml@4.3.2": {
"js-yaml": "private"
},
"path@0.12.7": {
"path": "private"
},
"@halo-dev/api-client@2.26.0(axios@1.20.0)": {
"@halo-dev/api-client": "private"
},
"yaml@2.9.0": {
"yaml": "private"
},
"lightningcss@1.33.0": {
"lightningcss": "private"
},
"picomatch@4.0.7": {
"picomatch": "private"
},
"postcss@8.5.26": {
"postcss": "private"
},
"rolldown@1.2.6": {
"rolldown": "private"
},
"tinyglobby@0.2.17": {
"tinyglobby": "private"
},
"fsevents@2.3.3": {
"fsevents": "private"
},
"axios@1.20.0": {
"axios": "private"
},
"qs@6.16.0": {
"qs": "private"
},
"archiver-utils@5.0.2": {
"archiver-utils": "private"
},
"async@3.2.6": {
"async": "private"
},
"buffer-crc32@1.0.0": {
"buffer-crc32": "private"
},
"readable-stream@4.7.0": {
"readable-stream": "private"
},
"readdir-glob@1.1.3": {
"readdir-glob": "private"
},
"tar-stream@3.2.1": {
"tar-stream": "private"
},
"zip-stream@6.0.1": {
"zip-stream": "private"
},
"graceful-fs@4.2.11": {
"graceful-fs": "private"
},
"jsonfile@6.2.1": {
"jsonfile": "private"
},
"universalify@2.0.1": {
"universalify": "private"
},
"argparse@2.0.1": {
"argparse": "private"
},
"detect-libc@2.1.2": {
"detect-libc": "private"
},
"lightningcss-darwin-arm64@1.33.0": {
"lightningcss-darwin-arm64": "private"
},
"process@0.11.10": {
"process": "private"
},
"util@0.10.4": {
"util": "private"
},
"nanoid@3.3.18": {
"nanoid": "private"
},
"picocolors@1.1.1": {
"picocolors": "private"
},
"source-map-js@1.2.1": {
"source-map-js": "private"
},
"@oxc-project/types@0.147.0": {
"@oxc-project/types": "private"
},
"@rolldown/pluginutils@1.0.1": {
"@rolldown/pluginutils": "private"
},
"@rolldown/binding-darwin-arm64@1.2.6": {
"@rolldown/binding-darwin-arm64": "private"
},
"fdir@6.5.0(picomatch@4.0.7)": {
"fdir": "private"
},
"glob@10.4.5": {
"glob": "private"
},
"is-stream@2.0.1": {
"is-stream": "private"
},
"lazystream@1.0.1": {
"lazystream": "private"
},
"lodash@4.18.1": {
"lodash": "private"
},
"normalize-path@3.0.0": {
"normalize-path": "private"
},
"follow-redirects@1.16.0": {
"follow-redirects": "private"
},
"form-data@4.0.6": {
"form-data": "private"
},
"https-proxy-agent@5.0.1": {
"https-proxy-agent": "private"
},
"proxy-from-env@2.1.0": {
"proxy-from-env": "private"
},
"es-define-property@1.0.1": {
"es-define-property": "private"
},
"side-channel@1.1.1": {
"side-channel": "private"
},
"abort-controller@3.0.0": {
"abort-controller": "private"
},
"buffer@6.0.3": {
"buffer": "private"
},
"events@3.3.0": {
"events": "private"
},
"string_decoder@1.3.0": {
"string_decoder": "private"
},
"minimatch@5.1.9": {
"minimatch": "private"
},
"b4a@1.8.1": {
"b4a": "private"
},
"bare-fs@4.8.1": {
"bare-fs": "private"
},
"fast-fifo@1.3.2": {
"fast-fifo": "private"
},
"streamx@2.28.1": {
"streamx": "private"
},
"inherits@2.0.3": {
"inherits": "private"
},
"compress-commons@6.0.2": {
"compress-commons": "private"
},
"event-target-shim@5.0.1": {
"event-target-shim": "private"
},
"bare-events@2.9.2": {
"bare-events": "private"
},
"bare-path@3.1.2": {
"bare-path": "private"
},
"bare-stream@2.13.4(bare-events@2.9.2)": {
"bare-stream": "private"
},
"bare-url@2.5.4": {
"bare-url": "private"
},
"base64-js@1.5.1": {
"base64-js": "private"
},
"ieee754@1.2.1": {
"ieee754": "private"
},
"crc-32@1.2.2": {
"crc-32": "private"
},
"crc32-stream@6.0.0": {
"crc32-stream": "private"
},
"asynckit@0.4.0": {
"asynckit": "private"
},
"combined-stream@1.0.8": {
"combined-stream": "private"
},
"es-set-tostringtag@2.1.0": {
"es-set-tostringtag": "private"
},
"hasown@2.0.4": {
"hasown": "private"
},
"mime-types@2.1.35": {
"mime-types": "private"
},
"foreground-child@3.3.1": {
"foreground-child": "private"
},
"jackspeak@3.4.3": {
"jackspeak": "private"
},
"minipass@7.1.3": {
"minipass": "private"
},
"package-json-from-dist@1.0.1": {
"package-json-from-dist": "private"
},
"path-scurry@1.11.1": {
"path-scurry": "private"
},
"agent-base@6.0.2": {
"agent-base": "private"
},
"debug@4.4.3": {
"debug": "private"
},
"brace-expansion@2.1.4": {
"brace-expansion": "private"
},
"es-errors@1.3.0": {
"es-errors": "private"
},
"object-inspect@1.13.4": {
"object-inspect": "private"
},
"side-channel-list@1.0.1": {
"side-channel-list": "private"
},
"side-channel-map@1.0.1": {
"side-channel-map": "private"
},
"side-channel-weakmap@1.0.2": {
"side-channel-weakmap": "private"
},
"events-universal@1.0.1": {
"events-universal": "private"
},
"text-decoder@1.2.7": {
"text-decoder": "private"
},
"safe-buffer@5.2.1": {
"safe-buffer": "private"
},
"teex@1.0.1": {
"teex": "private"
},
"delayed-stream@1.0.0": {
"delayed-stream": "private"
},
"ms@2.1.3": {
"ms": "private"
},
"get-intrinsic@1.3.0": {
"get-intrinsic": "private"
},
"has-tostringtag@1.0.2": {
"has-tostringtag": "private"
},
"cross-spawn@7.0.6": {
"cross-spawn": "private"
},
"signal-exit@4.1.0": {
"signal-exit": "private"
},
"function-bind@1.1.2": {
"function-bind": "private"
},
"@isaacs/cliui@8.0.2": {
"@isaacs/cliui": "private"
},
"@pkgjs/parseargs@0.11.0": {
"@pkgjs/parseargs": "private"
},
"mime-db@1.52.0": {
"mime-db": "private"
},
"lru-cache@10.4.3": {
"lru-cache": "private"
},
"core-util-is@1.0.3": {
"core-util-is": "private"
},
"isarray@1.0.0": {
"isarray": "private"
},
"process-nextick-args@2.0.1": {
"process-nextick-args": "private"
},
"util-deprecate@1.0.2": {
"util-deprecate": "private"
},
"call-bound@1.0.4": {
"call-bound": "private"
},
"string-width@5.1.2": {
"string-width": "private"
},
"string-width@4.2.3": {
"string-width-cjs": "private"
},
"strip-ansi@7.2.0": {
"strip-ansi": "private"
},
"strip-ansi@6.0.1": {
"strip-ansi-cjs": "private"
},
"wrap-ansi@8.1.0": {
"wrap-ansi": "private"
},
"wrap-ansi@7.0.0": {
"wrap-ansi-cjs": "private"
},
"balanced-match@1.0.2": {
"balanced-match": "private"
},
"call-bind-apply-helpers@1.0.2": {
"call-bind-apply-helpers": "private"
},
"path-key@3.1.1": {
"path-key": "private"
},
"shebang-command@2.0.0": {
"shebang-command": "private"
},
"which@2.0.2": {
"which": "private"
},
"es-object-atoms@1.1.2": {
"es-object-atoms": "private"
},
"get-proto@1.0.1": {
"get-proto": "private"
},
"gopd@1.2.0": {
"gopd": "private"
},
"has-symbols@1.1.0": {
"has-symbols": "private"
},
"math-intrinsics@1.1.0": {
"math-intrinsics": "private"
},
"dunder-proto@1.0.1": {
"dunder-proto": "private"
},
"shebang-regex@3.0.0": {
"shebang-regex": "private"
},
"emoji-regex@8.0.0": {
"emoji-regex": "private"
},
"is-fullwidth-code-point@3.0.0": {
"is-fullwidth-code-point": "private"
},
"eastasianwidth@0.2.0": {
"eastasianwidth": "private"
},
"ansi-regex@5.0.1": {
"ansi-regex": "private"
},
"isexe@2.0.0": {
"isexe": "private"
},
"ansi-styles@4.3.0": {
"ansi-styles": "private"
},
"color-convert@2.0.1": {
"color-convert": "private"
},
"color-name@1.1.4": {
"color-name": "private"
}
},
"hoistPattern": [
"*"
],
"included": {
"dependencies": true,
"devDependencies": true,
"optionalDependencies": true
},
"injectedDeps": {},
"layoutVersion": 5,
"nodeLinker": "isolated",
"packageManager": "pnpm@10.33.0",
"pendingBuilds": [],
"publicHoistPattern": [],
"prunedAt": "Thu, 03 Sep 2026 11:19:07 GMT",
"registries": {
"default": "https://registry.npmmirror.com/",
"@jsr": "https://npm.jsr.io/"
},
"skipped": [
"@rolldown/binding-android-arm-eabi@1.2.6",
"@rolldown/binding-android-arm64@1.2.6",
"@rolldown/binding-darwin-x64@1.2.6",
"@rolldown/binding-freebsd-x64@1.2.6",
"@rolldown/binding-linux-arm-gnueabihf@1.2.6",
"@rolldown/binding-linux-arm64-gnu@1.2.6",
"@rolldown/binding-linux-arm64-musl@1.2.6",
"@rolldown/binding-linux-ppc64-gnu@1.2.6",
"@rolldown/binding-linux-s390x-gnu@1.2.6",
"@rolldown/binding-linux-x64-gnu@1.2.6",
"@rolldown/binding-linux-x64-musl@1.2.6",
"@rolldown/binding-openharmony-arm64@1.2.6",
"@rolldown/binding-win32-arm64-msvc@1.2.6",
"@rolldown/binding-win32-x64-msvc@1.2.6",
"lightningcss-android-arm64@1.33.0",
"lightningcss-darwin-x64@1.33.0",
"lightningcss-freebsd-x64@1.33.0",
"lightningcss-linux-arm-gnueabihf@1.33.0",
"lightningcss-linux-arm64-gnu@1.33.0",
"lightningcss-linux-arm64-musl@1.33.0",
"lightningcss-linux-x64-gnu@1.33.0",
"lightningcss-linux-x64-musl@1.33.0",
"lightningcss-win32-arm64-msvc@1.33.0",
"lightningcss-win32-x64-msvc@1.33.0"
],
"storeDir": "/Users/huanghui/Library/pnpm/store/v10",
"virtualStoreDir": ".pnpm",
"virtualStoreDirMaxLength": 120
}
+27
View File
@@ -0,0 +1,27 @@
{
"lastValidatedTimestamp": 1788434348044,
"projects": {},
"pnpmfiles": [],
"settings": {
"autoInstallPeers": true,
"dedupeDirectDeps": false,
"dedupeInjectedDeps": true,
"dedupePeerDependents": true,
"dedupePeers": false,
"dev": true,
"excludeLinksFromLockfile": false,
"hoistPattern": [
"*"
],
"hoistWorkspacePackages": true,
"injectWorkspacePackages": false,
"linkWorkspacePackages": false,
"nodeLinker": "isolated",
"optional": true,
"peersSuffixMaxLength": 1000,
"preferWorkspacePackages": false,
"production": true,
"publicHoistPattern": []
},
"filteredInstall": false
}
@@ -0,0 +1,69 @@
# @halo-dev/api-client
Halo 2.0 的 JavaScript API 客户端请求库。使用 [OpenAPI Generator](https://openapi-generator.tech/) 生成。
## 使用
```javascript
import {
coreApiClient,
consoleApiClient,
ucApiClient,
publicApiClient,
createCoreApiClient,
createConsoleApiClient,
createUcApiClient,
createPublicApiClient,
axiosInstance,
} from "@halo-dev/api-client";
```
- **coreApiClient**: 为 Halo 所有自定义模型的 CRUD 接口封装的 api client。
- **consoleApiClient**: 为 Halo 针对 Console 提供的接口封装的 api client。
- **ucApiClient**: 为 Halo 针对 UC 提供的接口封装的 api client。
- **publicApiClient**: 为 Halo 所有公开访问的接口封装的 api client。
- **createCoreApiClient**: 用于创建自定义模型的 CRUD 接口封装的 api client,需要传入 axios 实例。
- **createConsoleApiClient**: 用于创建 Console 接口封装的 api client,需要传入 axios 实例。
- **createUcApiClient**: 用于创建 UC 接口封装的 api client,需要传入 axios 实例。
- **createPublicApiClient**: 用于创建公开访问接口封装的 api client,需要传入 axios 实例。
- **axiosInstance**: 内部默认创建的 axios 实例。
### 在插件中使用
```shell
pnpm install @halo-dev/api-client axios
```
由于已经在 Console 和 UC 项目中引入并设置好了 Axios 拦截器,所以直接使用即可:
```javascript
import { coreApiClient } from "@halo-dev/api-client";
coreApiClient.content.post.listPost().then((response) => {
// handle response
});
```
此外,在最新的 `@halo-dev/ui-plugin-bundler-kit@2.17.0` 中,已经排除了 `@halo-dev/api-client``axios` 依赖,所以最终产物中的相关依赖会自动使用 Halo 本身提供的依赖,无需关心最终产物大小。
详细文档可查阅:[插件开发 / API 请求](https://docs.halo.run/developer-guide/plugin/api-reference/ui/api-request)
### 在外部项目中使用
```shell
pnpm install @halo-dev/api-client axios
```
```javascript
import axios from "axios";
const axiosInstance = axios.create({
baseURL: "http://localhost:8090",
});
const coreApiClient = createCoreApiClient(axiosInstance);
coreApiClient.content.post.listPost().then((response) => {
// handle response
});
```
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
{
"name": "@halo-dev/api-client",
"version": "2.26.0",
"description": "API Client for Halo 2",
"homepage": "https://github.com/halo-dev/halo/tree/main/ui/packages/api-client#readme",
"bugs": {
"url": "https://github.com/halo-dev/halo/issues"
},
"license": "GPL-3.0",
"author": "@halo-dev",
"contributors": [
{
"name": "Ryan Wang",
"url": "https://github.com/ruibaby"
}
],
"repository": {
"type": "git",
"url": "https://github.com/halo-dev/halo.git",
"directory": "ui/packages/api-client"
},
"files": [
"dist"
],
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/entry/index.d.ts"
]
}
},
"exports": {
".": "./dist/index.js",
"./package.json": "./package.json"
},
"dependencies": {
"qs": "^6.15.3"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.40.1",
"@types/qs": "^6.15.1",
"rimraf": "^5.0.10"
},
"peerDependencies": {
"axios": "^1.16.0"
},
"inlinedDependencies": {
"call-bind-apply-helpers": "1.0.2",
"call-bound": "1.0.4",
"dunder-proto": "1.0.1",
"es-define-property": "1.0.1",
"es-errors": "1.3.0",
"es-object-atoms": "1.1.2",
"function-bind": "1.1.2",
"get-intrinsic": "1.3.0",
"get-proto": "1.0.1",
"gopd": "1.2.0",
"has-symbols": "1.1.0",
"hasown": "2.0.4",
"math-intrinsics": "1.1.0",
"object-inspect": "1.13.4",
"qs": "6.15.3",
"side-channel": "1.1.1",
"side-channel-list": "1.0.1",
"side-channel-map": "1.0.1",
"side-channel-weakmap": "1.0.2"
},
"scripts": {
"build": "vp pack",
"gen": "rimraf --glob './src/**' && openapi-generator-cli generate -i ../../../api-docs/openapi/v3_0/aggregated.json -g typescript-axios -c ./.openapi_config.yaml -o ./src --type-mappings='set=Array' --global-property apiDocs=false,modelDocs=false"
}
}
@@ -0,0 +1,28 @@
name: Release NPM
permissions:
contents: write
id-token: write
on:
push:
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: lts/*
registry-url: https://registry.npmjs.org/
- uses: pnpm/action-setup@v5
- run: pnpm install
- name: Upgrade npm
run: npm i -g npm@latest
- name: Publish to NPM
run: npm publish
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"sortImports": {},
"sortPackageJson": true,
"insertFinalNewline": true,
"useTabs": false,
"ignorePatterns": []
}
@@ -0,0 +1,18 @@
{
"$schema": "https://unpkg.com/release-it@19/schema/release-it.json",
"git": {
"requireCleanWorkingDir": true,
"requireUpstream": true,
"commit": true,
"commitMessage": "Release v${version}",
"tag": true,
"tagName": "${version}",
"push": true
},
"github": {
"release": false
},
"npm": {
"publish": false
}
}
@@ -0,0 +1,67 @@
# @halo-dev/theme-package-cli
A command-line tool for packaging Halo theme template files, making them ready for distribution.
## Features
- Package Halo theme template files into a ZIP file
- Option to package all files or only essential files
## Installation
### Global Installation
```bash
npm install -g @halo-dev/theme-package-cli
# or
npx @halo-dev/theme-package-cli
```
### Local Installation
```bash
npm install @halo-dev/theme-package-cli
```
## Usage
Run the command in your Halo theme project root directory:
```bash
# Only package essential files (templates, README.md, *.yaml/*.yml, i18n, LICENSE, screenshot.*)
theme-package
# Package all files except common unnecessary files and directories
theme-package --all
# or
theme-package -a
```
### Packaging Rules
- By default, only packages essential files and directories:
- templates directory
- README.md
- All .yaml and .yml files
- i18n directory
- LICENSE file
- Root-level `screenshot.png`, `screenshot.jpeg`, `screenshot.jpg`, or `screenshot.webp`
- When using the `--all` parameter, packages all files in the project while excluding some common unnecessary files and directories:
- dist directory
- node_modules directories
- .git directory
- .github directory
- .idea directory
- .DS_Store files
## Requirements
- Node.js environment
- The theme project root directory must contain a properly formatted theme.yaml file
## License
MIT
@@ -0,0 +1,117 @@
#!/usr/bin/env node
import path from "path";
import archiver from "archiver";
import { program } from "commander";
import fs from "fs-extra";
import yaml from "js-yaml";
const packageJson = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"));
const ignorePatterns = [
"dist/**",
"node_modules/**",
"**/node_modules/**",
".git/**",
".github/**",
".idea/**",
".DS_Store",
"**/.DS_Store",
];
const essentialPaths = [
"templates/**",
"ui-plugin/dist/**",
"README.md",
"LICENSE",
"i18n/**",
"*.yaml",
"*.yml",
"screenshot.png",
"screenshot.jpeg",
"screenshot.jpg",
"screenshot.webp",
];
program
.version(packageJson.version)
.description("A CLI tool for packaging Halo theme template files");
program.option(
"-a, --all",
"Package all files, excluding some unnecessary directories and files",
false,
);
program.parse(process.argv);
const options = program.opts();
async function main() {
try {
const themeYamlPath = path.join(process.cwd(), "theme.yaml");
if (!(await fs.pathExists(themeYamlPath))) {
console.error("Error: theme.yaml file not found in the current directory");
process.exit(1);
}
const themeYamlContent = await fs.readFile(themeYamlPath, "utf8");
const themeConfig = yaml.load(themeYamlContent);
if (
!themeConfig?.metadata ||
!themeConfig.metadata.name ||
!themeConfig.spec ||
!themeConfig.spec.version
) {
console.error("Error: theme.yaml file format is incorrect, missing required fields");
process.exit(1);
}
const distDir = path.join(process.cwd(), "dist");
await fs.ensureDir(distDir);
const zipFileName = `${themeConfig.metadata.name}-${themeConfig.spec.version}.zip`;
const zipFilePath = path.join(distDir, zipFileName);
const output = fs.createWriteStream(zipFilePath);
const archive = archiver("zip", {
zlib: { level: 9 },
});
output.on("close", function () {
console.log(`✅ Packaged successfully: ${zipFilePath}`);
console.log(`Theme version: ${themeConfig.spec.version}`);
console.log(`File size: ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB`);
});
archive.on("error", function (err) {
throw err;
});
archive.pipe(output);
if (options.all) {
archive.glob("**/*", {
cwd: process.cwd(),
ignore: ignorePatterns,
dot: true,
});
} else {
essentialPaths.forEach((pattern) => {
archive.glob(pattern, {
cwd: process.cwd(),
ignore: ignorePatterns,
dot: true,
});
});
}
await archive.finalize();
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}
main();
@@ -0,0 +1,21 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/js-yaml@4.3.2/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/js-yaml@4.3.2/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../js-yaml@4.3.2/node_modules/js-yaml/bin/js-yaml.js" "$@"
else
exec node "$basedir/../../../../../../js-yaml@4.3.2/node_modules/js-yaml/bin/js-yaml.js" "$@"
fi
@@ -0,0 +1,44 @@
{
"name": "@halo-dev/theme-package-cli",
"version": "1.1.1",
"description": "A CLI tool for packaging Halo theme template files",
"keywords": [
"cli",
"halo",
"package",
"theme"
],
"license": "MIT",
"author": "@halo-dev",
"contributors": [
{
"name": "Ryan Wang",
"url": "https://github.com/ruibaby"
}
],
"repository": {
"type": "git",
"url": "https://github.com/halo-dev/theme-package-cli.git"
},
"bin": {
"theme-package": "./index.js"
},
"type": "module",
"main": "index.js",
"scripts": {
"fmt": "oxfmt",
"release": "release-it"
},
"dependencies": {
"archiver": "^7.0.1",
"commander": "^13.1.0",
"fs-extra": "^11.3.4",
"js-yaml": "^4.1.1",
"path": "^0.12.7"
},
"devDependencies": {
"oxfmt": "^0.41.0",
"release-it": "^19.2.4"
},
"packageManager": "pnpm@10.32.1"
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Halo SIGs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,163 @@
# @halo-dev/vite-plugin-halo-theme
[简体中文](./README.zh.md)
A Vite plugin built specifically for Halo theme development.
It precompiles templates before Vite processes HTML, adds lightweight template reuse features (`include`, `slot`, `props`) for Halo themes, and automatically treats HTML files in `src` as multi-page entries output to `templates`.
## Why This Plugin
Halo themes often need reusable page fragments and layout composition.
This plugin fills that gap during precompilation, reducing template duplication and improving maintainability.
Main capabilities:
1. Template reuse features: supports `include`, `slot`, `props`, and related semantics.
2. Automatically scans the `src` directory, uses all HTML files as Vite build entries, and handles static asset imports.
## Install
```bash
pnpm add -D @halo-dev/vite-plugin-halo-theme
```
## Quick Start
Enable the plugin in `vite.config.ts`:
```ts
import { defineConfig } from "vite";
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";
export default defineConfig({
plugins: [haloThemePlugin()],
});
```
## Example
See the runnable example project in [`example`](./example).
```bash
cd example
pnpm install
pnpm build
```
## Directory Conventions
This plugin is Halo-specific and uses the following defaults:
- Page source directory: `src`
- Partials directory: `src/partials`
- Static assets directory: `public`
- Output directory: `templates`
- Output assets directory: `templates/assets`
Example structure:
```text
.
├── public
│ └── ...
├── src
│ ├── js
│ │ ├── index.js
│ │ └── post.js
│ ├── css
│ │ └── index.css
│ ├── index.html # page entry
│ ├── post.html # page entry
│ └── partials # template partials directory (convention)
│ ├── layout.html # partial template
│ └── card.html # partial template
├── theme.yaml
└── templates # output directory for templates and assets
```
## Entry Scanning Rules
The plugin scans all `.html` files under `src` and skips `src/partials`.
Mapping examples:
- `src/index.html` -> `templates/index.html`
- `src/post.html` -> `templates/post.html`
## Template Syntax
> Note: The syntax in this section (`include`, `slot`, `props`) is precompiled only during the Vite build phase. It is not native Thymeleaf syntax.
> Keep the two phases separate: Vite compile time (frontend build) vs Thymeleaf render time (backend runtime).
### Include
```html
<include src="layout.html">
<main>...</main>
</include>
```
### Props
Usage:
```html
<include src="card.html" title="Hello"></include>
```
In partial:
```html
<h2>{{title}}</h2>
```
### Default Slot
In partial:
```html
<slot />
```
### Named Slot
In partial:
```html
<slot name="head" />
```
Usage:
```html
<include src="layout.html">
<template name="head">
<title>Page Title</title>
</template>
<main>...</main>
</include>
```
## Include Path Resolution
- `./foo.html` / `../foo.html`: relative to current file
- `/foo.html`: relative to `src`
- `foo.html`: resolves `src/partials/foo.html` first
- `partials/foo.html`: resolves as `src/partials/foo.html`
## Development Commands
```bash
pnpm install
pnpm test
pnpm build
pnpm lint
pnpm fmt
```
## Compatibility and Boundaries
- The current parser is intentionally lightweight and focused on plugin syntax needs, not full HTML5 specification parsing.
- `{{prop}}` is string-level interpolation and does not evaluate expressions.
- This plugin is purpose-built for Halo themes, not a general template engine for all site stacks.
@@ -0,0 +1,161 @@
# @halo-dev/vite-plugin-halo-theme
一个面向 Halo 主题开发的 Vite 插件。
它在 Vite 处理 HTML 之前执行模板预编译,为 Halo 主题提供更轻量的组件复用能力(`include``slot``props`),并将 `src` 下的 HTML 自动作为多页面入口构建到 `templates`
## 为什么需要它
Halo 主题通常需要页面片段复用和布局组合能力。
这个插件通过预编译阶段补齐这类能力,降低模板重复,提升主题维护效率。
主要能力:
1. 模板复用能力:支持 include、slot、props 等
2. 自动扫描 `src` 目录,将所有 HTML 作为 Vite 构建入口,并自动处理静态资源引入
## 安装
```bash
pnpm add -D @halo-dev/vite-plugin-halo-theme
```
## 快速开始
`vite.config.ts` 中启用插件:
```ts
import { defineConfig } from "vite";
import { haloThemePlugin } from "@halo-dev/vite-plugin-halo-theme";
export default defineConfig({
plugins: [haloThemePlugin()],
});
```
## 示例项目
可运行示例位于 [`example`](./example)。
```bash
cd example
pnpm install
pnpm build
```
## 目录约定
本插件是 Halo 主题专用,默认约定:
- 页面目录:`src`
- partials 目录:`src/partials`
- 静态资源目录:`public`
- 输出目录:`templates`
- 构建资源目录:`templates/assets`
示例结构:
```text
.
├── public
│ └── ...
├── src
│ ├── js
│ │ ├── index.js
│ │ └── post.js
│ ├── css
│ │ └── index.css
│ ├── index.html # 页面入口
│ ├── post.html # 页面入口
│ └── partials # 模板片段目录(约定)
│ ├── layout.html # 模板片段
│ └── card.html # 模板片段
├── theme.yaml
└── templates # 模板与静态资源输出目录
```
## 入口扫描规则
插件会扫描 `src` 下全部 `.html`,并跳过 `src/partials`
映射示例:
- `src/index.html` -> `templates/index.html`
- `src/post.html` -> `templates/post.html`
## 模板语法
> 注意:本节语法(`include`、`slot`、`props`)仅在 Vite 构建阶段由插件预编译处理,不是 Thymeleaf 原生语法。
> 请区分两套处理时机:Vite 编译期(前端构建)与 Thymeleaf 渲染期(后端运行时)。
### Include
```html
<include src="layout.html">
<main>...</main>
</include>
```
### Props
调用:
```html
<include src="card.html" title="Hello"></include>
```
partial
```html
<h2>{{title}}</h2>
```
### 默认 Slot
partial
```html
<slot />
```
### 具名 Slot
partial
```html
<slot name="head" />
```
调用:
```html
<include src="layout.html">
<template name="head">
<title>Page Title</title>
</template>
<main>...</main>
</include>
```
## include 路径解析
- `./foo.html` / `../foo.html`:相对当前文件
- `/foo.html`:相对 `src`
- `foo.html`:优先 `src/partials/foo.html`
- `partials/foo.html`:解析为 `src/partials/foo.html`
## 开发命令
```bash
pnpm install
pnpm test
pnpm build
pnpm lint
pnpm fmt
```
## 兼容性与边界
- 当前 parser 为轻量实现,面向插件语法需求,不是完整 HTML5 规范解析器
- `{{prop}}` 为字符串级插值,不执行表达式
- 插件定位为 Halo 主题专用,不追求通用站点模板引擎场景
@@ -0,0 +1,6 @@
import { Plugin } from "vite";
//#region src/index.d.ts
declare function haloThemePlugin(): Plugin;
//#endregion
export { haloThemePlugin };
@@ -0,0 +1,583 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { parse } from "yaml";
//#region package.json
var name = "@halo-dev/vite-plugin-halo-theme";
//#endregion
//#region src/template-compiler/generator.ts
function generateTemplate(root) {
return root.children.map((node) => generateNode(node)).join("");
}
function generateNode(node) {
if (node.type === "text" || node.type === "comment" || node.type === "doctype") return node.value;
if (node.selfClosing || node.rawCloseTag === null) return node.rawOpenTag;
return `${node.rawOpenTag}${node.children.map((child) => generateNode(child)).join("")}${node.rawCloseTag}`;
}
//#endregion
//#region src/template-compiler/parser.ts
function parseTemplate(input) {
const root = {
type: "root",
loc: createRange(input, 0, input.length),
children: []
};
const stack = [];
let cursor = 0;
const pushNode = (node) => {
const parent = stack[stack.length - 1];
if (parent) {
parent.children.push(node);
return;
}
root.children.push(node);
};
while (cursor < input.length) {
if (input.startsWith("<!--", cursor)) {
const endIndex = input.indexOf("-->", cursor + 4);
const end = endIndex === -1 ? input.length : endIndex + 3;
pushNode({
type: "comment",
value: input.slice(cursor, end),
loc: createRange(input, cursor, end)
});
cursor = end;
continue;
}
if (input[cursor] === "<") {
if (input.startsWith("</", cursor)) {
const closeTagEnd = findTagEnd(input, cursor);
if (closeTagEnd === -1) {
pushNode({
type: "text",
value: input.slice(cursor),
loc: createRange(input, cursor, input.length)
});
break;
}
const rawCloseTag = input.slice(cursor, closeTagEnd + 1);
const open = closeTopMatchingElement(stack, readCloseTagName(rawCloseTag));
if (!open) {
pushNode({
type: "text",
value: rawCloseTag,
loc: createRange(input, cursor, closeTagEnd + 1)
});
cursor = closeTagEnd + 1;
continue;
}
pushNode({
type: "element",
tagName: open.tagName,
rawOpenTag: open.rawOpenTag,
rawCloseTag,
attributes: open.attributes,
children: open.children,
selfClosing: false,
loc: createRange(input, open.startOffset, closeTagEnd + 1)
});
cursor = closeTagEnd + 1;
continue;
}
if (looksLikeDoctype(input, cursor)) {
const doctypeEnd = findTagEnd(input, cursor);
const end = doctypeEnd === -1 ? input.length : doctypeEnd + 1;
pushNode({
type: "doctype",
value: input.slice(cursor, end),
loc: createRange(input, cursor, end)
});
cursor = end;
continue;
}
const openTagEnd = findTagEnd(input, cursor);
if (openTagEnd === -1) {
pushNode({
type: "text",
value: input.slice(cursor),
loc: createRange(input, cursor, input.length)
});
break;
}
const rawOpenTag = input.slice(cursor, openTagEnd + 1);
const parsedTag = parseOpenTag(rawOpenTag);
if (!parsedTag) {
pushNode({
type: "text",
value: rawOpenTag,
loc: createRange(input, cursor, openTagEnd + 1)
});
cursor = openTagEnd + 1;
continue;
}
if (parsedTag.selfClosing) {
pushNode({
type: "element",
tagName: parsedTag.tagName,
rawOpenTag,
rawCloseTag: null,
attributes: parsedTag.attributes,
children: [],
selfClosing: true,
loc: createRange(input, cursor, openTagEnd + 1)
});
cursor = openTagEnd + 1;
continue;
}
stack.push({
tagName: parsedTag.tagName,
rawOpenTag,
attributes: parsedTag.attributes,
selfClosing: false,
startOffset: cursor,
openTagEndOffset: openTagEnd + 1,
children: []
});
cursor = openTagEnd + 1;
continue;
}
const nextTagStart = input.indexOf("<", cursor);
const textEnd = nextTagStart === -1 ? input.length : nextTagStart;
pushNode({
type: "text",
value: input.slice(cursor, textEnd),
loc: createRange(input, cursor, textEnd)
});
cursor = textEnd;
}
while (stack.length > 0) {
const open = stack.pop();
if (!open) continue;
pushNode({
type: "element",
tagName: open.tagName,
rawOpenTag: open.rawOpenTag,
rawCloseTag: null,
attributes: open.attributes,
children: open.children,
selfClosing: false,
loc: createRange(input, open.startOffset, open.openTagEndOffset)
});
}
return root;
}
function parseOpenTag(openTag) {
const inner = openTag.slice(1, openTag.endsWith("/>") ? -2 : -1).trim();
if (inner.length === 0 || inner.startsWith("/")) return null;
const [rawTagName, ...restParts] = inner.split(/\s+/);
if (!rawTagName) return null;
return {
tagName: rawTagName.toLowerCase(),
attributes: parseAttributes(restParts.length > 0 ? inner.slice(rawTagName.length).trim() : ""),
selfClosing: /\/\s*>$/.test(openTag)
};
}
function parseAttributes(attrSource) {
const attributes = [];
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
let match;
while ((match = pattern.exec(attrSource)) !== null) {
const [, name, doubleQuotedValue, singleQuotedValue] = match;
attributes.push({
name,
value: doubleQuotedValue ?? singleQuotedValue ?? ""
});
}
return attributes;
}
function readCloseTagName(rawCloseTag) {
return /^<\/\s*([^\s>]+)\s*>$/.exec(rawCloseTag)?.[1]?.toLowerCase() ?? "";
}
function closeTopMatchingElement(stack, closeTagName) {
if (closeTagName.length === 0) return null;
for (let index = stack.length - 1; index >= 0; index -= 1) {
if (stack[index]?.tagName !== closeTagName) continue;
const [matched] = stack.splice(index, 1);
return matched ?? null;
}
return null;
}
function findTagEnd(input, start) {
let inSingleQuote = false;
let inDoubleQuote = false;
for (let index = start; index < input.length; index += 1) {
const char = input[index];
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}
if (char === "\"" && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
}
if (char === ">" && !inSingleQuote && !inDoubleQuote) return index;
}
return -1;
}
function looksLikeDoctype(input, start) {
return /^<!doctype\b/i.test(input.slice(start, start + 10));
}
function createRange(input, startOffset, endOffset) {
return {
start: offsetToPosition(input, startOffset),
end: offsetToPosition(input, endOffset)
};
}
function offsetToPosition(input, offset) {
let line = 1;
let column = 1;
const end = Math.max(0, Math.min(offset, input.length));
for (let index = 0; index < end; index += 1) {
if (input[index] === "\n") {
line += 1;
column = 1;
continue;
}
column += 1;
}
return {
offset: end,
line,
column
};
}
//#endregion
//#region src/template-compiler/compiler.ts
const DEFAULT_SLOT_NAME = "default";
const PARTIALS_DIR$1 = "partials";
const TEMPLATE_LOG_PREFIX = `[${name}]`;
function compileTemplate(html, context) {
const root = parseTemplate(html);
root.children = transformNodes(root.children, context);
return generateTemplate(root);
}
function createRenderContext(input) {
return {
options: input.options,
currentFile: input.currentFile,
stack: input.stack ?? [input.currentFile]
};
}
function transformNodes(nodes, context) {
const output = [];
for (const node of nodes) {
if (node.type !== "element") {
output.push(node);
continue;
}
if (node.tagName === "include") {
output.push(...expandInclude(node, context));
continue;
}
node.children = transformNodes(node.children, context);
output.push(node);
}
return output;
}
function expandInclude(node, context) {
const attributes = toAttributeMap(node.attributes);
const rawSource = attributes.src;
if (!rawSource) return [createErrorTextNode("<!-- Partial error: missing src attribute -->", node)];
const fullPath = resolvePartialPath(rawSource, context);
if (!fullPath) return [createErrorTextNode(`<!-- Partial error: Could not load ${rawSource} -->`, node)];
if (context.stack.includes(fullPath)) {
console.error(`${TEMPLATE_LOG_PREFIX} Circular partial include detected: ${[...context.stack, fullPath].join(" -> ")}`);
return [createErrorTextNode(`<!-- Partial error: Circular include ${rawSource} -->`, node)];
}
try {
let content = readFileSync(fullPath, "utf-8");
const props = { ...attributes };
delete props.src;
content = interpolateProps(content, props);
const partialRoot = parseTemplate(content);
applySlots(partialRoot, extractSlots(node.children));
const childContext = {
options: context.options,
currentFile: fullPath,
stack: [...context.stack, fullPath]
};
partialRoot.children = transformNodes(partialRoot.children, childContext);
return partialRoot.children;
} catch (error) {
logTemplateError("Error reading partial", context.currentFile, node, String(error));
return [createErrorTextNode(`<!-- Partial error: Could not load ${rawSource} -->`, node)];
}
}
function extractSlots(includeChildren) {
const namedNodes = {};
const defaultNodes = cloneNodes(includeChildren);
if (!removeNamedTemplateNodes(defaultNodes, namedNodes)) return {
defaultNodes,
namedNodes
};
return {
defaultNodes,
namedNodes
};
}
function removeNamedTemplateNodes(nodes, namedNodes) {
let hasTemplateMatch = false;
let index = 0;
while (index < nodes.length) {
const node = nodes[index];
if (node?.type !== "element") {
index += 1;
continue;
}
if (node.tagName === "include") {
index += 1;
continue;
}
if (node.tagName === "template") {
hasTemplateMatch = true;
const slotName = toAttributeMap(node.attributes).name;
if (slotName) {
namedNodes[slotName] = cloneNodes(node.children);
nodes.splice(index, 1);
continue;
}
index += 1;
continue;
}
if (node.children.length > 0) {
const nestedMatched = removeNamedTemplateNodes(node.children, namedNodes);
hasTemplateMatch ||= nestedMatched;
}
index += 1;
}
return hasTemplateMatch;
}
function applySlots(root, slots) {
root.children = replaceSlotNodes(root.children, slots);
}
function replaceSlotNodes(nodes, slots) {
const output = [];
const hasDefaultContent = serializeNodes(slots.defaultNodes).trim().length > 0;
for (const node of nodes) {
if (node.type !== "element") {
output.push(node);
continue;
}
if (node.tagName === "slot") {
const slotName = toAttributeMap(node.attributes).name ?? DEFAULT_SLOT_NAME;
if (slotName === DEFAULT_SLOT_NAME) {
const replacement = hasDefaultContent ? slots.defaultNodes : node.children;
output.push(...cloneNodes(replacement));
continue;
}
if (Object.prototype.hasOwnProperty.call(slots.namedNodes, slotName)) {
output.push(...cloneNodes(slots.namedNodes[slotName] ?? []));
continue;
}
output.push(...cloneNodes(node.children));
continue;
}
node.children = replaceSlotNodes(node.children, slots);
output.push(node);
}
return output;
}
function resolvePartialPath(rawSource, context) {
const normalizedSource = rawSource.replace(/\\/g, "/");
const candidates = /* @__PURE__ */ new Set();
if (normalizedSource.startsWith("./") || normalizedSource.startsWith("../")) candidates.add(path.resolve(path.dirname(context.currentFile), normalizedSource));
else if (normalizedSource.startsWith("/")) candidates.add(path.resolve(context.options.srcRoot, normalizedSource.slice(1)));
else {
candidates.add(path.resolve(context.options.partialsRoot, normalizedSource));
if (normalizedSource.startsWith(`${PARTIALS_DIR$1}/`)) candidates.add(path.resolve(context.options.srcRoot, normalizedSource));
else candidates.add(path.resolve(path.dirname(context.currentFile), normalizedSource));
}
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
console.error(`${TEMPLATE_LOG_PREFIX} Partial not found: ${rawSource}`);
return null;
}
function interpolateProps(content, props) {
return content.replace(/\{\{(\w+)\}\}/g, (_match, key) => props[key] ?? "");
}
function toAttributeMap(attributes) {
const map = {};
for (const attribute of attributes) map[attribute.name] = attribute.value;
return map;
}
function createErrorTextNode(message, baseNode) {
return {
type: "text",
value: message,
loc: baseNode.loc
};
}
function cloneNodes(nodes) {
return nodes.map((node) => cloneNode(node));
}
function cloneNode(node) {
if (node.type !== "element") return {
...node,
loc: {
...node.loc,
start: { ...node.loc.start },
end: { ...node.loc.end }
}
};
return {
...node,
attributes: node.attributes.map((attribute) => ({ ...attribute })),
children: node.children.map((child) => cloneNode(child)),
loc: {
...node.loc,
start: { ...node.loc.start },
end: { ...node.loc.end }
}
};
}
function serializeNodes(nodes) {
return generateTemplate({
type: "root",
loc: {
start: {
offset: 0,
line: 1,
column: 1
},
end: {
offset: 0,
line: 1,
column: 1
}
},
children: nodes
});
}
function logTemplateError(message, sourceFile, includeNode, detail) {
const line = includeNode.loc.start.line;
const column = includeNode.loc.start.column;
console.error(`${TEMPLATE_LOG_PREFIX} ${message} at ${sourceFile}:${line}:${column}`);
console.error(`${TEMPLATE_LOG_PREFIX} ${detail}`);
}
//#endregion
//#region src/index.ts
const SRC_DIR = "src";
const PARTIALS_DIR = "partials";
const OUT_DIR = "templates";
const ASSETS_DIR = "assets";
const EMPTY_OUT_DIR = true;
const HTML_EXTENSION = ".html";
const PLUGIN_LOG_PREFIX = `[${name}]`;
function haloThemePlugin() {
const resolvedOptions = resolveHaloThemeBuildOptions();
const compilerOptions = {
srcRoot: resolvedOptions.srcRoot,
partialsRoot: resolvedOptions.partialsRoot
};
return {
name: "vite-plugin-halo-theme",
config() {
return {
root: resolvedOptions.srcRoot,
publicDir: path.resolve(resolvedOptions.projectRoot, "public"),
base: resolvedOptions.base,
build: {
outDir: path.resolve(resolvedOptions.projectRoot, OUT_DIR),
assetsDir: ASSETS_DIR,
emptyOutDir: EMPTY_OUT_DIR,
rollupOptions: { input: collectThemeTemplateEntries(resolvedOptions) }
}
};
},
transformIndexHtml: {
order: "pre",
handler(html, ctx) {
const currentFile = resolveCurrentHtmlFile(ctx, resolvedOptions);
return compileTemplate(html, createRenderContext({
options: compilerOptions,
currentFile,
stack: [currentFile]
}));
}
}
};
}
function resolveHaloThemeBuildOptions() {
const projectRoot = process.cwd();
return {
base: `/themes/${readHaloThemeName(projectRoot)}`,
projectRoot,
srcRoot: path.resolve(projectRoot, SRC_DIR),
partialsRoot: path.resolve(projectRoot, SRC_DIR, PARTIALS_DIR)
};
}
function readHaloThemeName(projectRoot) {
const themeConfigPath = path.resolve(projectRoot, "theme.yaml");
if (!existsSync(themeConfigPath)) throw new Error(`${PLUGIN_LOG_PREFIX} Halo theme config not found: ${themeConfigPath}`);
const themeName = parse(readFileSync(themeConfigPath, "utf-8"))?.metadata?.name;
if (typeof themeName !== "string" || themeName.trim().length === 0) throw new Error(`${PLUGIN_LOG_PREFIX} Could not read metadata.name from ${themeConfigPath}`);
return themeName.trim();
}
function resolveCurrentHtmlFile(ctx, options) {
for (const resolver of htmlFileResolvers) {
const candidate = resolver(ctx, options);
if (candidate) return candidate;
}
return path.resolve(options.srcRoot, "index.html");
}
const htmlFileResolvers = [resolveHtmlFileFromFilename, resolveHtmlFileFromPath];
function resolveHtmlFileFromFilename(ctx) {
if (!("filename" in ctx)) return null;
if (typeof ctx.filename !== "string" || ctx.filename.length === 0) return null;
return ctx.filename;
}
function resolveHtmlFileFromPath(ctx, options) {
if (!("path" in ctx) || typeof ctx.path !== "string") return null;
const normalizedRequestPath = ctx.path.replace(/^\/+/, "");
return path.resolve(options.srcRoot, normalizedRequestPath || "index.html");
}
function collectThemeTemplateEntries(options) {
const entries = {};
const scanQueue = [{
directory: options.srcRoot,
relativeDirectory: ""
}];
while (scanQueue.length > 0) {
const current = scanQueue.shift();
if (!current) continue;
let items;
try {
items = readdirSync(current.directory, {
withFileTypes: true,
encoding: "utf8"
});
} catch (error) {
logEntryScanError(current.directory, error);
continue;
}
for (const item of items) {
const fullPath = path.resolve(current.directory, item.name);
const relativePath = current.relativeDirectory ? `${current.relativeDirectory}/${item.name}` : item.name;
if (item.isDirectory()) {
if (shouldSkipEntryDirectory(item.name)) continue;
scanQueue.push({
directory: fullPath,
relativeDirectory: relativePath
});
continue;
}
if (!item.isFile() || !isHtmlTemplateFile(item.name)) continue;
entries[toEntryName(relativePath)] = fullPath;
}
}
return entries;
}
function shouldSkipEntryDirectory(directoryName) {
return directoryName === PARTIALS_DIR;
}
function isHtmlTemplateFile(fileName) {
return fileName.endsWith(HTML_EXTENSION);
}
function toEntryName(relativeHtmlPath) {
const htmlPathWithoutExtension = relativeHtmlPath.replace(/\\/g, "/").slice(0, -5);
return (htmlPathWithoutExtension.endsWith("/index") ? htmlPathWithoutExtension.slice(0, -6) || "index" : htmlPathWithoutExtension).replace(/\//g, "_");
}
function logEntryScanError(directory, error) {
console.warn(`${PLUGIN_LOG_PREFIX} Failed to scan template directory: ${directory}`);
console.error(error);
}
//#endregion
export { haloThemePlugin };
@@ -0,0 +1,21 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules/vite/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules/vite/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/vite@8.2.2_yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../vite@8.2.2_yaml@2.9.0/node_modules/vite/bin/vite.js" "$@"
else
exec node "$basedir/../../../../../../vite@8.2.2_yaml@2.9.0/node_modules/vite/bin/vite.js" "$@"
fi
@@ -0,0 +1,21 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/yaml@2.9.0/node_modules:/Users/huanghui/halo2-dev/themes/theme-mdocs/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../../../../../yaml@2.9.0/node_modules/yaml/bin.mjs" "$@"
else
exec node "$basedir/../../../../../../yaml@2.9.0/node_modules/yaml/bin.mjs" "$@"
fi
@@ -0,0 +1,71 @@
{
"name": "@halo-dev/vite-plugin-halo-theme",
"version": "1.0.3",
"description": "A Vite plugin for Halo theme that provides HTML template pre-compilation and multi-page entry support.",
"keywords": [
"halo",
"halo-theme",
"vite",
"vite-plugin"
],
"bugs": {
"url": "https://github.com/halo-sigs/vite-plugin-halo-theme/issues"
},
"license": "MIT",
"author": {
"name": "Halo",
"url": "https://github.com/halo-dev"
},
"repository": {
"type": "git",
"url": "https://github.com/halo-sigs/vite-plugin-halo-theme.git"
},
"files": [
"dist",
"package.json"
],
"type": "module",
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"scripts": {
"build": "vp pack",
"prepack": "vp pack",
"test": "vp test run",
"test:watch": "vp test",
"fmt": "vp fmt",
"lint": "vp lint",
"prepare": "vp config",
"prepublishOnly": "vp pack",
"release": "release-it"
},
"dependencies": {
"@halo-dev/api-client": "^2.23.0",
"yaml": "^2.8.3"
},
"devDependencies": {
"@types/node": "^24.12.0",
"@typescript/native-preview": "7.0.0-dev.20260311.1",
"release-it": "^19.2.4",
"typescript": "^5.9.3",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"vite": "^7.0.0 || ^8.0.0"
},
"packageManager": "pnpm@10.33.0",
"compatiblePackages": {
"schemaVersion": 1,
"rolldown": {
"type": "incompatible",
"reason": "Uses Vite-specific APIs"
},
"rollup": {
"type": "incompatible",
"reason": "Uses Vite-specific APIs"
}
}
}
@@ -0,0 +1,14 @@
Copyright (c) 2015, Contributors
Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice
appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,143 @@
# @isaacs/cliui
Temporary fork of [cliui](http://npm.im/cliui).
![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg)
[![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui)
easily create complex multi-column command-line-interfaces.
## Example
```js
const ui = require('cliui')()
ui.div('Usage: $0 [command] [options]')
ui.div({
text: 'Options:',
padding: [2, 0, 1, 0]
})
ui.div(
{
text: "-f, --file",
width: 20,
padding: [0, 4, 0, 4]
},
{
text: "the file to load." +
chalk.green("(if this description is long it wraps).")
,
width: 20
},
{
text: chalk.red("[required]"),
align: 'right'
}
)
console.log(ui.toString())
```
## Deno/ESM Support
As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and
[ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules):
```typescript
import cliui from "https://deno.land/x/cliui/deno.ts";
const ui = cliui({})
ui.div('Usage: $0 [command] [options]')
ui.div({
text: 'Options:',
padding: [2, 0, 1, 0]
})
ui.div({
text: "-f, --file",
width: 20,
padding: [0, 4, 0, 4]
})
console.log(ui.toString())
```
<img width="500" src="screenshot.png">
## Layout DSL
cliui exposes a simple layout DSL:
If you create a single `ui.div`, passing a string rather than an
object:
* `\n`: characters will be interpreted as new rows.
* `\t`: characters will be interpreted as new columns.
* `\s`: characters will be interpreted as padding.
**as an example...**
```js
var ui = require('./')({
width: 60
})
ui.div(
'Usage: node ./bin/foo.js\n' +
' <regex>\t provide a regex\n' +
' <glob>\t provide a glob\t [required]'
)
console.log(ui.toString())
```
**will output:**
```shell
Usage: node ./bin/foo.js
<regex> provide a regex
<glob> provide a glob [required]
```
## Methods
```js
cliui = require('cliui')
```
### cliui({width: integer})
Specify the maximum width of the UI being generated.
If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`.
### cliui({wrap: boolean})
Enable or disable the wrapping of text in a column.
### cliui.div(column, column, column)
Create a row with any number of columns, a column
can either be a string, or an object with the following
options:
* **text:** some text to place in the column.
* **width:** the width of a column.
* **align:** alignment, `right` or `center`.
* **padding:** `[top, right, bottom, left]`.
* **border:** should a border be placed around the div?
### cliui.span(column, column, column)
Similar to `div`, except the next row will be appended without
a new line being created.
### cliui.resetOutput()
Resets the UI elements of the current cliui instance, maintaining the values
set for `width` and `wrap`.
@@ -0,0 +1,317 @@
'use strict';
const align = {
right: alignRight,
center: alignCenter
};
const top = 0;
const right = 1;
const bottom = 2;
const left = 3;
class UI {
constructor(opts) {
var _a;
this.width = opts.width;
/* c8 ignore start */
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
/* c8 ignore stop */
this.rows = [];
}
span(...args) {
const cols = this.div(...args);
cols.span = true;
}
resetOutput() {
this.rows = [];
}
div(...args) {
if (args.length === 0) {
this.div('');
}
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
return this.applyLayoutDSL(args[0]);
}
const cols = args.map(arg => {
if (typeof arg === 'string') {
return this.colFromString(arg);
}
return arg;
});
this.rows.push(cols);
return cols;
}
shouldApplyLayoutDSL(...args) {
return args.length === 1 && typeof args[0] === 'string' &&
/[\t\n]/.test(args[0]);
}
applyLayoutDSL(str) {
const rows = str.split('\n').map(row => row.split('\t'));
let leftColumnWidth = 0;
// simple heuristic for layout, make sure the
// second column lines up along the left-hand.
// don't allow the first column to take up more
// than 50% of the screen.
rows.forEach(columns => {
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
}
});
// generate a table:
// replacing ' ' with padding calculations.
// using the algorithmically generated width.
rows.forEach(columns => {
this.div(...columns.map((r, i) => {
return {
text: r.trim(),
padding: this.measurePadding(r),
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
};
}));
});
return this.rows[this.rows.length - 1];
}
colFromString(text) {
return {
text,
padding: this.measurePadding(text)
};
}
measurePadding(str) {
// measure padding without ansi escape codes
const noAnsi = mixin.stripAnsi(str);
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
}
toString() {
const lines = [];
this.rows.forEach(row => {
this.rowToString(row, lines);
});
// don't display any lines with the
// hidden flag set.
return lines
.filter(line => !line.hidden)
.map(line => line.text)
.join('\n');
}
rowToString(row, lines) {
this.rasterize(row).forEach((rrow, r) => {
let str = '';
rrow.forEach((col, c) => {
const { width } = row[c]; // the width with padding.
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
let ts = col; // temporary string used during alignment/padding.
if (wrapWidth > mixin.stringWidth(col)) {
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
}
// align the string within its column.
if (row[c].align && row[c].align !== 'left' && this.wrap) {
const fn = align[row[c].align];
ts = fn(ts, wrapWidth);
if (mixin.stringWidth(ts) < wrapWidth) {
/* c8 ignore start */
const w = width || 0;
/* c8 ignore stop */
ts += ' '.repeat(w - mixin.stringWidth(ts) - 1);
}
}
// apply border and padding to string.
const padding = row[c].padding || [0, 0, 0, 0];
if (padding[left]) {
str += ' '.repeat(padding[left]);
}
str += addBorder(row[c], ts, '| ');
str += ts;
str += addBorder(row[c], ts, ' |');
if (padding[right]) {
str += ' '.repeat(padding[right]);
}
// if prior row is span, try to render the
// current row on the prior line.
if (r === 0 && lines.length > 0) {
str = this.renderInline(str, lines[lines.length - 1]);
}
});
// remove trailing whitespace.
lines.push({
text: str.replace(/ +$/, ''),
span: row.span
});
});
return lines;
}
// if the full 'source' can render in
// the target line, do so.
renderInline(source, previousLine) {
const match = source.match(/^ */);
/* c8 ignore start */
const leadingWhitespace = match ? match[0].length : 0;
/* c8 ignore stop */
const target = previousLine.text;
const targetTextWidth = mixin.stringWidth(target.trimEnd());
if (!previousLine.span) {
return source;
}
// if we're not applying wrapping logic,
// just always append to the span.
if (!this.wrap) {
previousLine.hidden = true;
return target + source;
}
if (leadingWhitespace < targetTextWidth) {
return source;
}
previousLine.hidden = true;
return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart();
}
rasterize(row) {
const rrows = [];
const widths = this.columnWidths(row);
let wrapped;
// word wrap all columns, and create
// a data-structure that is easy to rasterize.
row.forEach((col, c) => {
// leave room for left and right padding.
col.width = widths[c];
if (this.wrap) {
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
}
else {
wrapped = col.text.split('\n');
}
if (col.border) {
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
}
// add top and bottom padding.
if (col.padding) {
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
}
wrapped.forEach((str, r) => {
if (!rrows[r]) {
rrows.push([]);
}
const rrow = rrows[r];
for (let i = 0; i < c; i++) {
if (rrow[i] === undefined) {
rrow.push('');
}
}
rrow.push(str);
});
});
return rrows;
}
negatePadding(col) {
/* c8 ignore start */
let wrapWidth = col.width || 0;
/* c8 ignore stop */
if (col.padding) {
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
}
if (col.border) {
wrapWidth -= 4;
}
return wrapWidth;
}
columnWidths(row) {
if (!this.wrap) {
return row.map(col => {
return col.width || mixin.stringWidth(col.text);
});
}
let unset = row.length;
let remainingWidth = this.width;
// column widths can be set in config.
const widths = row.map(col => {
if (col.width) {
unset--;
remainingWidth -= col.width;
return col.width;
}
return undefined;
});
// any unset widths should be calculated.
/* c8 ignore start */
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
/* c8 ignore stop */
return widths.map((w, i) => {
if (w === undefined) {
return Math.max(unsetWidth, _minWidth(row[i]));
}
return w;
});
}
}
function addBorder(col, ts, style) {
if (col.border) {
if (/[.']-+[.']/.test(ts)) {
return '';
}
if (ts.trim().length !== 0) {
return style;
}
return ' ';
}
return '';
}
// calculates the minimum width of
// a column, based on padding preferences.
function _minWidth(col) {
const padding = col.padding || [];
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
if (col.border) {
return minWidth + 4;
}
return minWidth;
}
function getWindowWidth() {
/* c8 ignore start */
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
return process.stdout.columns;
}
return 80;
}
/* c8 ignore stop */
function alignRight(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
if (strWidth < width) {
return ' '.repeat(width - strWidth) + str;
}
return str;
}
function alignCenter(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
/* c8 ignore start */
if (strWidth >= width) {
return str;
}
/* c8 ignore stop */
return ' '.repeat((width - strWidth) >> 1) + str;
}
let mixin;
function cliui(opts, _mixin) {
mixin = _mixin;
return new UI({
/* c8 ignore start */
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
/* c8 ignore stop */
});
}
// Bootstrap cliui with CommonJS dependencies:
const stringWidth = require('string-width-cjs');
const stripAnsi = require('strip-ansi-cjs');
const wrap = require('wrap-ansi-cjs');
function ui(opts) {
return cliui(opts, {
stringWidth,
stripAnsi,
wrap
});
}
module.exports = ui;
@@ -0,0 +1,43 @@
interface UIOptions {
width: number;
wrap?: boolean;
rows?: string[];
}
interface Column {
text: string;
width?: number;
align?: "right" | "left" | "center";
padding: number[];
border?: boolean;
}
interface ColumnArray extends Array<Column> {
span: boolean;
}
interface Line {
hidden?: boolean;
text: string;
span?: boolean;
}
declare class UI {
width: number;
wrap: boolean;
rows: ColumnArray[];
constructor(opts: UIOptions);
span(...args: ColumnArray): void;
resetOutput(): void;
div(...args: (Column | string)[]): ColumnArray;
private shouldApplyLayoutDSL;
private applyLayoutDSL;
private colFromString;
private measurePadding;
toString(): string;
rowToString(row: ColumnArray, lines: Line[]): Line[];
// if the full 'source' can render in
// the target line, do so.
private renderInline;
private rasterize;
private negatePadding;
private columnWidths;
}
declare function ui(opts: UIOptions): UI;
export { ui as default };
@@ -0,0 +1,302 @@
'use strict';
const align = {
right: alignRight,
center: alignCenter
};
const top = 0;
const right = 1;
const bottom = 2;
const left = 3;
export class UI {
constructor(opts) {
var _a;
this.width = opts.width;
/* c8 ignore start */
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
/* c8 ignore stop */
this.rows = [];
}
span(...args) {
const cols = this.div(...args);
cols.span = true;
}
resetOutput() {
this.rows = [];
}
div(...args) {
if (args.length === 0) {
this.div('');
}
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
return this.applyLayoutDSL(args[0]);
}
const cols = args.map(arg => {
if (typeof arg === 'string') {
return this.colFromString(arg);
}
return arg;
});
this.rows.push(cols);
return cols;
}
shouldApplyLayoutDSL(...args) {
return args.length === 1 && typeof args[0] === 'string' &&
/[\t\n]/.test(args[0]);
}
applyLayoutDSL(str) {
const rows = str.split('\n').map(row => row.split('\t'));
let leftColumnWidth = 0;
// simple heuristic for layout, make sure the
// second column lines up along the left-hand.
// don't allow the first column to take up more
// than 50% of the screen.
rows.forEach(columns => {
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
}
});
// generate a table:
// replacing ' ' with padding calculations.
// using the algorithmically generated width.
rows.forEach(columns => {
this.div(...columns.map((r, i) => {
return {
text: r.trim(),
padding: this.measurePadding(r),
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
};
}));
});
return this.rows[this.rows.length - 1];
}
colFromString(text) {
return {
text,
padding: this.measurePadding(text)
};
}
measurePadding(str) {
// measure padding without ansi escape codes
const noAnsi = mixin.stripAnsi(str);
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
}
toString() {
const lines = [];
this.rows.forEach(row => {
this.rowToString(row, lines);
});
// don't display any lines with the
// hidden flag set.
return lines
.filter(line => !line.hidden)
.map(line => line.text)
.join('\n');
}
rowToString(row, lines) {
this.rasterize(row).forEach((rrow, r) => {
let str = '';
rrow.forEach((col, c) => {
const { width } = row[c]; // the width with padding.
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
let ts = col; // temporary string used during alignment/padding.
if (wrapWidth > mixin.stringWidth(col)) {
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
}
// align the string within its column.
if (row[c].align && row[c].align !== 'left' && this.wrap) {
const fn = align[row[c].align];
ts = fn(ts, wrapWidth);
if (mixin.stringWidth(ts) < wrapWidth) {
/* c8 ignore start */
const w = width || 0;
/* c8 ignore stop */
ts += ' '.repeat(w - mixin.stringWidth(ts) - 1);
}
}
// apply border and padding to string.
const padding = row[c].padding || [0, 0, 0, 0];
if (padding[left]) {
str += ' '.repeat(padding[left]);
}
str += addBorder(row[c], ts, '| ');
str += ts;
str += addBorder(row[c], ts, ' |');
if (padding[right]) {
str += ' '.repeat(padding[right]);
}
// if prior row is span, try to render the
// current row on the prior line.
if (r === 0 && lines.length > 0) {
str = this.renderInline(str, lines[lines.length - 1]);
}
});
// remove trailing whitespace.
lines.push({
text: str.replace(/ +$/, ''),
span: row.span
});
});
return lines;
}
// if the full 'source' can render in
// the target line, do so.
renderInline(source, previousLine) {
const match = source.match(/^ */);
/* c8 ignore start */
const leadingWhitespace = match ? match[0].length : 0;
/* c8 ignore stop */
const target = previousLine.text;
const targetTextWidth = mixin.stringWidth(target.trimEnd());
if (!previousLine.span) {
return source;
}
// if we're not applying wrapping logic,
// just always append to the span.
if (!this.wrap) {
previousLine.hidden = true;
return target + source;
}
if (leadingWhitespace < targetTextWidth) {
return source;
}
previousLine.hidden = true;
return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart();
}
rasterize(row) {
const rrows = [];
const widths = this.columnWidths(row);
let wrapped;
// word wrap all columns, and create
// a data-structure that is easy to rasterize.
row.forEach((col, c) => {
// leave room for left and right padding.
col.width = widths[c];
if (this.wrap) {
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
}
else {
wrapped = col.text.split('\n');
}
if (col.border) {
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
}
// add top and bottom padding.
if (col.padding) {
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
}
wrapped.forEach((str, r) => {
if (!rrows[r]) {
rrows.push([]);
}
const rrow = rrows[r];
for (let i = 0; i < c; i++) {
if (rrow[i] === undefined) {
rrow.push('');
}
}
rrow.push(str);
});
});
return rrows;
}
negatePadding(col) {
/* c8 ignore start */
let wrapWidth = col.width || 0;
/* c8 ignore stop */
if (col.padding) {
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
}
if (col.border) {
wrapWidth -= 4;
}
return wrapWidth;
}
columnWidths(row) {
if (!this.wrap) {
return row.map(col => {
return col.width || mixin.stringWidth(col.text);
});
}
let unset = row.length;
let remainingWidth = this.width;
// column widths can be set in config.
const widths = row.map(col => {
if (col.width) {
unset--;
remainingWidth -= col.width;
return col.width;
}
return undefined;
});
// any unset widths should be calculated.
/* c8 ignore start */
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
/* c8 ignore stop */
return widths.map((w, i) => {
if (w === undefined) {
return Math.max(unsetWidth, _minWidth(row[i]));
}
return w;
});
}
}
function addBorder(col, ts, style) {
if (col.border) {
if (/[.']-+[.']/.test(ts)) {
return '';
}
if (ts.trim().length !== 0) {
return style;
}
return ' ';
}
return '';
}
// calculates the minimum width of
// a column, based on padding preferences.
function _minWidth(col) {
const padding = col.padding || [];
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
if (col.border) {
return minWidth + 4;
}
return minWidth;
}
function getWindowWidth() {
/* c8 ignore start */
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
return process.stdout.columns;
}
return 80;
}
/* c8 ignore stop */
function alignRight(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
if (strWidth < width) {
return ' '.repeat(width - strWidth) + str;
}
return str;
}
function alignCenter(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
/* c8 ignore start */
if (strWidth >= width) {
return str;
}
/* c8 ignore stop */
return ' '.repeat((width - strWidth) >> 1) + str;
}
let mixin;
export function cliui(opts, _mixin) {
mixin = _mixin;
return new UI({
/* c8 ignore start */
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
/* c8 ignore stop */
});
}
@@ -0,0 +1,14 @@
// Bootstrap cliui with ESM dependencies:
import { cliui } from './build/lib/index.js'
import stringWidth from 'string-width'
import stripAnsi from 'strip-ansi'
import wrap from 'wrap-ansi'
export default function ui (opts) {
return cliui(opts, {
stringWidth,
stripAnsi,
wrap
})
}
@@ -0,0 +1,86 @@
{
"name": "@isaacs/cliui",
"version": "8.0.2",
"description": "easily create complex multi-column command-line-interfaces",
"main": "build/index.cjs",
"exports": {
".": [
{
"import": "./index.mjs",
"require": "./build/index.cjs"
},
"./build/index.cjs"
]
},
"type": "module",
"module": "./index.mjs",
"scripts": {
"check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'",
"fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'",
"pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs",
"test": "c8 mocha ./test/*.cjs",
"test:esm": "c8 mocha ./test/**/*.mjs",
"postest": "check",
"coverage": "c8 report --check-coverage",
"precompile": "rimraf build",
"compile": "tsc",
"postcompile": "npm run build:cjs",
"build:cjs": "rollup -c",
"prepare": "npm run compile"
},
"repository": "yargs/cliui",
"standard": {
"ignore": [
"**/example/**"
],
"globals": [
"it"
]
},
"keywords": [
"cli",
"command-line",
"layout",
"design",
"console",
"wrap",
"table"
],
"author": "Ben Coe <ben@npmjs.com>",
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"devDependencies": {
"@types/node": "^14.0.27",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^4.0.0",
"c8": "^7.3.0",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"cross-env": "^7.0.2",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"gts": "^3.0.0",
"mocha": "^10.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.23.1",
"rollup-plugin-ts": "^3.0.2",
"standardx": "^7.0.0",
"typescript": "^4.0.0"
},
"files": [
"build",
"index.mjs",
"!*.d.ts"
],
"engines": {
"node": ">=12"
}
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024-present VoidZero Inc. & Contributors
Copyright (c) 2023 Boshen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,3 @@
# Oxc Types
Typescript definitions for Oxc AST nodes.
@@ -0,0 +1,26 @@
{
"name": "@oxc-project/types",
"version": "0.147.0",
"description": "Types for Oxc AST nodes",
"keywords": [
"AST",
"Parser"
],
"homepage": "https://oxc.rs",
"bugs": "https://github.com/oxc-project/oxc/issues",
"license": "MIT",
"author": "Boshen and oxc contributors",
"repository": {
"type": "git",
"url": "git+https://github.com/oxc-project/oxc.git",
"directory": "npm/oxc-types"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"files": [
"types.d.ts"
],
"type": "module",
"types": "types.d.ts"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Copied from Node.js to ease compatibility in PR.
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single
@@ -0,0 +1,147 @@
# Changelog
## [0.11.0](https://github.com/pkgjs/parseargs/compare/v0.10.0...v0.11.0) (2022-10-08)
### Features
* add `default` option parameter ([#142](https://github.com/pkgjs/parseargs/issues/142)) ([cd20847](https://github.com/pkgjs/parseargs/commit/cd20847a00b2f556aa9c085ac83b942c60868ec1))
## [0.10.0](https://github.com/pkgjs/parseargs/compare/v0.9.1...v0.10.0) (2022-07-21)
### Features
* add parsed meta-data to returned properties ([#129](https://github.com/pkgjs/parseargs/issues/129)) ([91bfb4d](https://github.com/pkgjs/parseargs/commit/91bfb4d3f7b6937efab1b27c91c45d1205f1497e))
## [0.9.1](https://github.com/pkgjs/parseargs/compare/v0.9.0...v0.9.1) (2022-06-20)
### Bug Fixes
* **runtime:** support node 14+ ([#135](https://github.com/pkgjs/parseargs/issues/135)) ([6a1c5a6](https://github.com/pkgjs/parseargs/commit/6a1c5a6f7cadf2f035e004027e2742e3c4ce554b))
## [0.9.0](https://github.com/pkgjs/parseargs/compare/v0.8.0...v0.9.0) (2022-05-23)
### ⚠ BREAKING CHANGES
* drop handling of electron arguments (#121)
### Code Refactoring
* drop handling of electron arguments ([#121](https://github.com/pkgjs/parseargs/issues/121)) ([a2ffd53](https://github.com/pkgjs/parseargs/commit/a2ffd537c244a062371522b955acb45a404fc9f2))
## [0.8.0](https://github.com/pkgjs/parseargs/compare/v0.7.1...v0.8.0) (2022-05-16)
### ⚠ BREAKING CHANGES
* switch type:string option arguments to greedy, but with error for suspect cases in strict mode (#88)
* positionals now opt-in when strict:true (#116)
* create result.values with null prototype (#111)
### Features
* create result.values with null prototype ([#111](https://github.com/pkgjs/parseargs/issues/111)) ([9d539c3](https://github.com/pkgjs/parseargs/commit/9d539c3d57f269c160e74e0656ad4fa84ff92ec2))
* positionals now opt-in when strict:true ([#116](https://github.com/pkgjs/parseargs/issues/116)) ([3643338](https://github.com/pkgjs/parseargs/commit/364333826b746e8a7dc5505b4b22fd19ac51df3b))
* switch type:string option arguments to greedy, but with error for suspect cases in strict mode ([#88](https://github.com/pkgjs/parseargs/issues/88)) ([c2b5e72](https://github.com/pkgjs/parseargs/commit/c2b5e72161991dfdc535909f1327cc9b970fe7e8))
### [0.7.1](https://github.com/pkgjs/parseargs/compare/v0.7.0...v0.7.1) (2022-04-15)
### Bug Fixes
* resist pollution ([#106](https://github.com/pkgjs/parseargs/issues/106)) ([ecf2dec](https://github.com/pkgjs/parseargs/commit/ecf2dece0a9f2a76d789384d5d71c68ffe64022a))
## [0.7.0](https://github.com/pkgjs/parseargs/compare/v0.6.0...v0.7.0) (2022-04-13)
### Features
* Add strict mode to parser ([#74](https://github.com/pkgjs/parseargs/issues/74)) ([8267d02](https://github.com/pkgjs/parseargs/commit/8267d02083a87b8b8a71fcce08348d1e031ea91c))
## [0.6.0](https://github.com/pkgjs/parseargs/compare/v0.5.0...v0.6.0) (2022-04-11)
### ⚠ BREAKING CHANGES
* rework results to remove redundant `flags` property and store value true for boolean options (#83)
* switch to existing ERR_INVALID_ARG_VALUE (#97)
### Code Refactoring
* rework results to remove redundant `flags` property and store value true for boolean options ([#83](https://github.com/pkgjs/parseargs/issues/83)) ([be153db](https://github.com/pkgjs/parseargs/commit/be153dbed1d488cb7b6e27df92f601ba7337713d))
* switch to existing ERR_INVALID_ARG_VALUE ([#97](https://github.com/pkgjs/parseargs/issues/97)) ([084a23f](https://github.com/pkgjs/parseargs/commit/084a23f9fde2da030b159edb1c2385f24579ce40))
## [0.5.0](https://github.com/pkgjs/parseargs/compare/v0.4.0...v0.5.0) (2022-04-10)
### ⚠ BREAKING CHANGES
* Require type to be specified for each supplied option (#95)
### Features
* Require type to be specified for each supplied option ([#95](https://github.com/pkgjs/parseargs/issues/95)) ([02cd018](https://github.com/pkgjs/parseargs/commit/02cd01885b8aaa59f2db8308f2d4479e64340068))
## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12)
### ⚠ BREAKING CHANGES
* parsing, revisit short option groups, add support for combined short and value (#75)
* restructure configuration to take options bag (#63)
### Code Refactoring
* parsing, revisit short option groups, add support for combined short and value ([#75](https://github.com/pkgjs/parseargs/issues/75)) ([a92600f](https://github.com/pkgjs/parseargs/commit/a92600fa6c214508ab1e016fa55879a314f541af))
* restructure configuration to take options bag ([#63](https://github.com/pkgjs/parseargs/issues/63)) ([b412095](https://github.com/pkgjs/parseargs/commit/b4120957d90e809ee8b607b06e747d3e6a6b213e))
## [0.3.0](https://github.com/pkgjs/parseargs/compare/v0.2.0...v0.3.0) (2022-02-06)
### Features
* **parser:** support short-option groups ([#59](https://github.com/pkgjs/parseargs/issues/59)) ([882067b](https://github.com/pkgjs/parseargs/commit/882067bc2d7cbc6b796f8e5a079a99bc99d4e6ba))
## [0.2.0](https://github.com/pkgjs/parseargs/compare/v0.1.1...v0.2.0) (2022-02-05)
### Features
* basic support for shorts ([#50](https://github.com/pkgjs/parseargs/issues/50)) ([a2f36d7](https://github.com/pkgjs/parseargs/commit/a2f36d7da4145af1c92f76806b7fe2baf6beeceb))
### Bug Fixes
* always store value for a=b ([#43](https://github.com/pkgjs/parseargs/issues/43)) ([a85e8dc](https://github.com/pkgjs/parseargs/commit/a85e8dc06379fd2696ee195cc625de8fac6aee42))
* support single dash as positional ([#49](https://github.com/pkgjs/parseargs/issues/49)) ([d795bf8](https://github.com/pkgjs/parseargs/commit/d795bf877d068fd67aec381f30b30b63f97109ad))
### [0.1.1](https://github.com/pkgjs/parseargs/compare/v0.1.0...v0.1.1) (2022-01-25)
### Bug Fixes
* only use arrays in results for multiples ([#42](https://github.com/pkgjs/parseargs/issues/42)) ([c357584](https://github.com/pkgjs/parseargs/commit/c357584847912506319ed34a0840080116f4fd65))
## 0.1.0 (2022-01-22)
### Features
* expand scenarios covered by default arguments for environments ([#20](https://github.com/pkgjs/parseargs/issues/20)) ([582ada7](https://github.com/pkgjs/parseargs/commit/582ada7be0eca3a73d6e0bd016e7ace43449fa4c))
* update readme and include contributing guidelines ([8edd6fc](https://github.com/pkgjs/parseargs/commit/8edd6fc863cd705f6fac732724159ebe8065a2b0))
### Bug Fixes
* do not strip excess leading dashes on long option names ([#21](https://github.com/pkgjs/parseargs/issues/21)) ([f848590](https://github.com/pkgjs/parseargs/commit/f848590ebf3249ed5979ff47e003fa6e1a8ec5c0))
* name & readme ([3f057c1](https://github.com/pkgjs/parseargs/commit/3f057c1b158a1bdbe878c64b57460c58e56e465f))
* package.json values ([9bac300](https://github.com/pkgjs/parseargs/commit/9bac300e00cd76c77076bf9e75e44f8929512da9))
* update readme name ([957d8d9](https://github.com/pkgjs/parseargs/commit/957d8d96e1dcb48297c0a14345d44c0123b2883e))
### Build System
* first release as minor ([421c6e2](https://github.com/pkgjs/parseargs/commit/421c6e2569a8668ad14fac5a5af5be60479a7571))
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,413 @@
<!-- omit in toc -->
# parseArgs
[![Coverage][coverage-image]][coverage-url]
Polyfill of `util.parseArgs()`
## `util.parseArgs([config])`
<!-- YAML
added: v18.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/43459
description: add support for returning detailed parse information
using `tokens` in input `config` and returned properties.
-->
> Stability: 1 - Experimental
* `config` {Object} Used to provide arguments for parsing and to configure
the parser. `config` supports the following properties:
* `args` {string\[]} array of argument strings. **Default:** `process.argv`
with `execPath` and `filename` removed.
* `options` {Object} Used to describe arguments known to the parser.
Keys of `options` are the long names of options and values are an
{Object} accepting the following properties:
* `type` {string} Type of argument, which must be either `boolean` or `string`.
* `multiple` {boolean} Whether this option can be provided multiple
times. If `true`, all values will be collected in an array. If
`false`, values for the option are last-wins. **Default:** `false`.
* `short` {string} A single character alias for the option.
* `default` {string | boolean | string\[] | boolean\[]} The default option
value when it is not set by args. It must be of the same type as the
the `type` property. When `multiple` is `true`, it must be an array.
* `strict` {boolean} Should an error be thrown when unknown arguments
are encountered, or when arguments are passed that do not match the
`type` configured in `options`.
**Default:** `true`.
* `allowPositionals` {boolean} Whether this command accepts positional
arguments.
**Default:** `false` if `strict` is `true`, otherwise `true`.
* `tokens` {boolean} Return the parsed tokens. This is useful for extending
the built-in behavior, from adding additional checks through to reprocessing
the tokens in different ways.
**Default:** `false`.
* Returns: {Object} The parsed command line arguments:
* `values` {Object} A mapping of parsed option names with their {string}
or {boolean} values.
* `positionals` {string\[]} Positional arguments.
* `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens)
section. Only returned if `config` includes `tokens: true`.
Provides a higher level API for command-line argument parsing than interacting
with `process.argv` directly. Takes a specification for the expected arguments
and returns a structured object with the parsed options and positionals.
```mjs
import { parseArgs } from 'node:util';
const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f'
},
bar: {
type: 'string'
}
};
const {
values,
positionals
} = parseArgs({ args, options });
console.log(values, positionals);
// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
```
```cjs
const { parseArgs } = require('node:util');
const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f'
},
bar: {
type: 'string'
}
};
const {
values,
positionals
} = parseArgs({ args, options });
console.log(values, positionals);
// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
```
`util.parseArgs` is experimental and behavior may change. Join the
conversation in [pkgjs/parseargs][] to contribute to the design.
### `parseArgs` `tokens`
Detailed parse information is available for adding custom behaviours by
specifying `tokens: true` in the configuration.
The returned tokens have properties describing:
* all tokens
* `kind` {string} One of 'option', 'positional', or 'option-terminator'.
* `index` {number} Index of element in `args` containing token. So the
source argument for a token is `args[token.index]`.
* option tokens
* `name` {string} Long name of option.
* `rawName` {string} How option used in args, like `-f` of `--foo`.
* `value` {string | undefined} Option value specified in args.
Undefined for boolean options.
* `inlineValue` {boolean | undefined} Whether option value specified inline,
like `--foo=bar`.
* positional tokens
* `value` {string} The value of the positional argument in args (i.e. `args[index]`).
* option-terminator token
The returned tokens are in the order encountered in the input args. Options
that appear more than once in args produce a token for each use. Short option
groups like `-xy` expand to a token for each option. So `-xxx` produces
three tokens.
For example to use the returned tokens to add support for a negated option
like `--no-color`, the tokens can be reprocessed to change the value stored
for the negated option.
```mjs
import { parseArgs } from 'node:util';
const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});
const color = values.color;
const logfile = values.logfile ?? 'default.log';
console.log({ logfile, color });
```
```cjs
const { parseArgs } = require('node:util');
const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});
const color = values.color;
const logfile = values.logfile ?? 'default.log';
console.log({ logfile, color });
```
Example usage showing negated options, and when an option is used
multiple ways then last one wins.
```console
$ node negate.js
{ logfile: 'default.log', color: undefined }
$ node negate.js --no-logfile --no-color
{ logfile: false, color: false }
$ node negate.js --logfile=test.log --color
{ logfile: 'test.log', color: true }
$ node negate.js --no-logfile --logfile=test.log --color --no-color
{ logfile: 'test.log', color: false }
```
-----
<!-- omit in toc -->
## Table of Contents
- [`util.parseArgs([config])`](#utilparseargsconfig)
- [Scope](#scope)
- [Version Matchups](#version-matchups)
- [🚀 Getting Started](#-getting-started)
- [🙌 Contributing](#-contributing)
- [💡 `process.mainArgs` Proposal](#-processmainargs-proposal)
- [Implementation:](#implementation)
- [📃 Examples](#-examples)
- [F.A.Qs](#faqs)
- [Links & Resources](#links--resources)
-----
## Scope
It is already possible to build great arg parsing modules on top of what Node.js provides; the prickly API is abstracted away by these modules. Thus, process.parseArgs() is not necessarily intended for library authors; it is intended for developers of simple CLI tools, ad-hoc scripts, deployed Node.js applications, and learning materials.
It is exceedingly difficult to provide an API which would both be friendly to these Node.js users while being extensible enough for libraries to build upon. We chose to prioritize these use cases because these are currently not well-served by Node.js' API.
----
## Version Matchups
| Node.js | @pkgjs/parseArgs |
| -- | -- |
| [v18.3.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [v0.9.1](https://github.com/pkgjs/parseargs/tree/v0.9.1#utilparseargsconfig) |
| [v16.17.0](https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilparseargsconfig), [v18.7.0](https://nodejs.org/docs/latest-v18.x/api/util.html#utilparseargsconfig) | [0.10.0](https://github.com/pkgjs/parseargs/tree/v0.10.0#utilparseargsconfig) |
----
## 🚀 Getting Started
1. **Install dependencies.**
```bash
npm install
```
2. **Open the index.js file and start editing!**
3. **Test your code by calling parseArgs through our test file**
```bash
npm test
```
----
## 🙌 Contributing
Any person who wants to contribute to the initiative is welcome! Please first read the [Contributing Guide](CONTRIBUTING.md)
Additionally, reading the [`Examples w/ Output`](#-examples-w-output) section of this document will be the best way to familiarize yourself with the target expected behavior for parseArgs() once it is fully implemented.
This package was implemented using [tape](https://www.npmjs.com/package/tape) as its test harness.
----
## 💡 `process.mainArgs` Proposal
> Note: This can be moved forward independently of the `util.parseArgs()` proposal/work.
### Implementation:
```javascript
process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
```
----
## 📃 Examples
```js
const { parseArgs } = require('@pkgjs/parseargs');
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// specify the options that may be used
const options = {
foo: { type: 'string'},
bar: { type: 'boolean' },
};
const args = ['--foo=a', '--bar'];
const { values, positionals } = parseArgs({ args, options });
// values = { foo: 'a', bar: true }
// positionals = []
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// type:string & multiple
const options = {
foo: {
type: 'string',
multiple: true,
},
};
const args = ['--foo=a', '--foo', 'b'];
const { values, positionals } = parseArgs({ args, options });
// values = { foo: [ 'a', 'b' ] }
// positionals = []
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// shorts
const options = {
foo: {
short: 'f',
type: 'boolean'
},
};
const args = ['-f', 'b'];
const { values, positionals } = parseArgs({ args, options, allowPositionals: true });
// values = { foo: true }
// positionals = ['b']
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// unconfigured
const options = {};
const args = ['-f', '--foo=a', '--bar', 'b'];
const { values, positionals } = parseArgs({ strict: false, args, options, allowPositionals: true });
// values = { f: true, foo: 'a', bar: true }
// positionals = ['b']
```
----
## F.A.Qs
- Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`?
- yes
- Does the parser execute a function?
- no
- Does the parser execute one of several functions, depending on input?
- no
- Can subcommands take options that are distinct from the main command?
- no
- Does it output generated help when no options match?
- no
- Does it generated short usage? Like: `usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]`
- no (no usage/help at all)
- Does the user provide the long usage text? For each option? For the whole command?
- no
- Do subcommands (if implemented) have their own usage output?
- no
- Does usage print if the user runs `cmd --help`?
- no
- Does it set `process.exitCode`?
- no
- Does usage print to stderr or stdout?
- N/A
- Does it check types? (Say, specify that an option is a boolean, number, etc.)
- no
- Can an option have more than one type? (string or false, for example)
- no
- Can the user define a type? (Say, `type: path` to call `path.resolve()` on the argument.)
- no
- Does a `--foo=0o22` mean 0, 22, 18, or "0o22"?
- `"0o22"`
- Does it coerce types?
- no
- Does `--no-foo` coerce to `--foo=false`? For all options? Only boolean options?
- no, it sets `{values:{'no-foo': true}}`
- Is `--foo` the same as `--foo=true`? Only for known booleans? Only at the end?
- no, they are not the same. There is no special handling of `true` as a value so it is just another string.
- Does it read environment variables? Ie, is `FOO=1 cmd` the same as `cmd --foo=1`?
- no
- Do unknown arguments raise an error? Are they parsed? Are they treated as positional arguments?
- no, they are parsed, not treated as positionals
- Does `--` signal the end of options?
- yes
- Is `--` included as a positional?
- no
- Is `program -- foo` the same as `program foo`?
- yes, both store `{positionals:['foo']}`
- Does the API specify whether a `--` was present/relevant?
- no
- Is `-bar` the same as `--bar`?
- no, `-bar` is a short option or options, with expansion logic that follows the
[Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `-bar` expands to `-b`, `-a`, `-r`.
- Is `---foo` the same as `--foo`?
- no
- the first is a long option named `'-foo'`
- the second is a long option named `'foo'`
- Is `-` a positional? ie, `bash some-test.sh | tap -`
- yes
## Links & Resources
* [Initial Tooling Issue](https://github.com/nodejs/tooling/issues/19)
* [Initial Proposal](https://github.com/nodejs/node/pull/35015)
* [parseArgs Proposal](https://github.com/nodejs/node/pull/42675)
[coverage-image]: https://img.shields.io/nycrc/pkgjs/parseargs
[coverage-url]: https://github.com/pkgjs/parseargs/blob/main/.nycrc
[pkgjs/parseargs]: https://github.com/pkgjs/parseargs
@@ -0,0 +1,25 @@
'use strict';
// This example shows how to understand if a default value is used or not.
// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo
const options = {
file: { short: 'f', type: 'string', default: 'FOO' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
const isFileDefault = !tokens.some((token) => token.kind === 'option' &&
token.name === 'file'
);
console.log(values);
console.log(`Is the file option [${values.file}] the default value? ${isFileDefault}`);
// Try the following:
// node is-default-value.js
// node is-default-value.js -f FILE
// node is-default-value.js --file FILE
@@ -0,0 +1,35 @@
'use strict';
// This is an example of using tokens to add a custom behaviour.
//
// Require the use of `=` for long options and values by blocking
// the use of space separated values.
// So allow `--foo=bar`, and not allow `--foo bar`.
//
// Note: this is not a common behaviour, most CLIs allow both forms.
// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo
const options = {
file: { short: 'f', type: 'string' },
log: { type: 'string' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
const badToken = tokens.find((token) => token.kind === 'option' &&
token.value != null &&
token.rawName.startsWith('--') &&
!token.inlineValue
);
if (badToken) {
throw new Error(`Option value for '${badToken.rawName}' must be inline, like '${badToken.rawName}=VALUE'`);
}
console.log(values);
// Try the following:
// node limit-long-syntax.js -f FILE --log=LOG
// node limit-long-syntax.js --file FILE
@@ -0,0 +1,43 @@
'use strict';
// This example is used in the documentation.
// How might I add my own support for --no-foo?
// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo
const options = {
'color': { type: 'boolean' },
'no-color': { type: 'boolean' },
'logfile': { type: 'string' },
'no-logfile': { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
// Reprocess the option tokens and overwrite the returned values.
tokens
.filter((token) => token.kind === 'option')
.forEach((token) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3);
values[positiveName] = false;
delete values[token.name];
} else {
// Resave value so last one wins if both --foo and --no-foo.
values[token.name] = token.value ?? true;
}
});
const color = values.color;
const logfile = values.logfile ?? 'default.log';
console.log({ logfile, color });
// Try the following:
// node negate.js
// node negate.js --no-logfile --no-color
// negate.js --logfile=test.log --color
// node negate.js --no-logfile --logfile=test.log --color --no-color
@@ -0,0 +1,31 @@
'use strict';
// This is an example of using tokens to add a custom behaviour.
//
// Throw an error if an option is used more than once.
// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo
const options = {
ding: { type: 'boolean', short: 'd' },
beep: { type: 'boolean', short: 'b' }
};
const { values, tokens } = parseArgs({ options, tokens: true });
const seenBefore = new Set();
tokens.forEach((token) => {
if (token.kind !== 'option') return;
if (seenBefore.has(token.name)) {
throw new Error(`option '${token.name}' used multiple times`);
}
seenBefore.add(token.name);
});
console.log(values);
// Try the following:
// node no-repeated-options --ding --beep
// node no-repeated-options --beep -b
// node no-repeated-options -ddd
@@ -0,0 +1,41 @@
// This is an example of using tokens to add a custom behaviour.
//
// This adds a option order check so that --some-unstable-option
// may only be used after --enable-experimental-options
//
// Note: this is not a common behaviour, the order of different options
// does not usually matter.
import { parseArgs } from '../index.js';
function findTokenIndex(tokens, target) {
return tokens.findIndex((token) => token.kind === 'option' &&
token.name === target
);
}
const experimentalName = 'enable-experimental-options';
const unstableName = 'some-unstable-option';
const options = {
[experimentalName]: { type: 'boolean' },
[unstableName]: { type: 'boolean' },
};
const { values, tokens } = parseArgs({ options, tokens: true });
const experimentalIndex = findTokenIndex(tokens, experimentalName);
const unstableIndex = findTokenIndex(tokens, unstableName);
if (unstableIndex !== -1 &&
((experimentalIndex === -1) || (unstableIndex < experimentalIndex))) {
throw new Error(`'--${experimentalName}' must be specified before '--${unstableName}'`);
}
console.log(values);
/* eslint-disable max-len */
// Try the following:
// node ordered-options.mjs
// node ordered-options.mjs --some-unstable-option
// node ordered-options.mjs --some-unstable-option --enable-experimental-options
// node ordered-options.mjs --enable-experimental-options --some-unstable-option
@@ -0,0 +1,26 @@
'use strict';
// This example is used in the documentation.
// 1. const { parseArgs } = require('node:util'); // from node
// 2. const { parseArgs } = require('@pkgjs/parseargs'); // from package
const { parseArgs } = require('..'); // in repo
const args = ['-f', '--bar', 'b'];
const options = {
foo: {
type: 'boolean',
short: 'f'
},
bar: {
type: 'string'
}
};
const {
values,
positionals
} = parseArgs({ args, options });
console.log(values, positionals);
// Try the following:
// node simple-hard-coded.js
@@ -0,0 +1,396 @@
'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeUnshiftApply,
ObjectEntries,
ObjectPrototypeHasOwnProperty: ObjectHasOwn,
StringPrototypeCharAt,
StringPrototypeIndexOf,
StringPrototypeSlice,
StringPrototypeStartsWith,
} = require('./internal/primordials');
const {
validateArray,
validateBoolean,
validateBooleanArray,
validateObject,
validateString,
validateStringArray,
validateUnion,
} = require('./internal/validators');
const {
kEmptyObject,
} = require('./internal/util');
const {
findLongOptionForShort,
isLoneLongOption,
isLoneShortOption,
isLongOptionAndValue,
isOptionValue,
isOptionLikeValue,
isShortOptionAndValue,
isShortOptionGroup,
useDefaultValueOption,
objectGetOwn,
optionsGetOwn,
} = require('./utils');
const {
codes: {
ERR_INVALID_ARG_VALUE,
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
ERR_PARSE_ARGS_UNKNOWN_OPTION,
ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
},
} = require('./internal/errors');
function getMainArgs() {
// Work out where to slice process.argv for user supplied arguments.
// Check node options for scenarios where user CLI args follow executable.
const execArgv = process.execArgv;
if (ArrayPrototypeIncludes(execArgv, '-e') ||
ArrayPrototypeIncludes(execArgv, '--eval') ||
ArrayPrototypeIncludes(execArgv, '-p') ||
ArrayPrototypeIncludes(execArgv, '--print')) {
return ArrayPrototypeSlice(process.argv, 1);
}
// Normally first two arguments are executable and script, then CLI arguments
return ArrayPrototypeSlice(process.argv, 2);
}
/**
* In strict mode, throw for possible usage errors like --foo --bar
*
* @param {object} token - from tokens as available from parseArgs
*/
function checkOptionLikeValue(token) {
if (!token.inlineValue && isOptionLikeValue(token.value)) {
// Only show short example if user used short option.
const example = StringPrototypeStartsWith(token.rawName, '--') ?
`'${token.rawName}=-XYZ'` :
`'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`;
const errorMessage = `Option '${token.rawName}' argument is ambiguous.
Did you forget to specify the option argument for '${token.rawName}'?
To specify an option argument starting with a dash use ${example}.`;
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage);
}
}
/**
* In strict mode, throw for usage errors.
*
* @param {object} config - from config passed to parseArgs
* @param {object} token - from tokens as available from parseArgs
*/
function checkOptionUsage(config, token) {
if (!ObjectHasOwn(config.options, token.name)) {
throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(
token.rawName, config.allowPositionals);
}
const short = optionsGetOwn(config.options, token.name, 'short');
const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`;
const type = optionsGetOwn(config.options, token.name, 'type');
if (type === 'string' && typeof token.value !== 'string') {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`);
}
// (Idiomatic test for undefined||null, expecting undefined.)
if (type === 'boolean' && token.value != null) {
throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`);
}
}
/**
* Store the option value in `values`.
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {string|undefined} optionValue - value from user args
* @param {object} options - option configs, from parseArgs({ options })
* @param {object} values - option values returned in `values` by parseArgs
*/
function storeOption(longOption, optionValue, options, values) {
if (longOption === '__proto__') {
return; // No. Just no.
}
// We store based on the option value rather than option type,
// preserving the users intent for author to deal with.
const newValue = optionValue ?? true;
if (optionsGetOwn(options, longOption, 'multiple')) {
// Always store value in array, including for boolean.
// values[longOption] starts out not present,
// first value is added as new array [newValue],
// subsequent values are pushed to existing array.
// (note: values has null prototype, so simpler usage)
if (values[longOption]) {
ArrayPrototypePush(values[longOption], newValue);
} else {
values[longOption] = [newValue];
}
} else {
values[longOption] = newValue;
}
}
/**
* Store the default option value in `values`.
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {string
* | boolean
* | string[]
* | boolean[]} optionValue - default value from option config
* @param {object} values - option values returned in `values` by parseArgs
*/
function storeDefaultOption(longOption, optionValue, values) {
if (longOption === '__proto__') {
return; // No. Just no.
}
values[longOption] = optionValue;
}
/**
* Process args and turn into identified tokens:
* - option (along with value, if any)
* - positional
* - option-terminator
*
* @param {string[]} args - from parseArgs({ args }) or mainArgs
* @param {object} options - option configs, from parseArgs({ options })
*/
function argsToTokens(args, options) {
const tokens = [];
let index = -1;
let groupCount = 0;
const remainingArgs = ArrayPrototypeSlice(args);
while (remainingArgs.length > 0) {
const arg = ArrayPrototypeShift(remainingArgs);
const nextArg = remainingArgs[0];
if (groupCount > 0)
groupCount--;
else
index++;
// Check if `arg` is an options terminator.
// Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
if (arg === '--') {
// Everything after a bare '--' is considered a positional argument.
ArrayPrototypePush(tokens, { kind: 'option-terminator', index });
ArrayPrototypePushApply(
tokens, ArrayPrototypeMap(remainingArgs, (arg) => {
return { kind: 'positional', index: ++index, value: arg };
})
);
break; // Finished processing args, leave while loop.
}
if (isLoneShortOption(arg)) {
// e.g. '-f'
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
let value;
let inlineValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
isOptionValue(nextArg)) {
// e.g. '-f', 'bar'
value = ArrayPrototypeShift(remainingArgs);
inlineValue = false;
}
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: arg,
index, value, inlineValue });
if (value != null) ++index;
continue;
}
if (isShortOptionGroup(arg, options)) {
// Expand -fXzy to -f -X -z -y
const expanded = [];
for (let index = 1; index < arg.length; index++) {
const shortOption = StringPrototypeCharAt(arg, index);
const longOption = findLongOptionForShort(shortOption, options);
if (optionsGetOwn(options, longOption, 'type') !== 'string' ||
index === arg.length - 1) {
// Boolean option, or last short in group. Well formed.
ArrayPrototypePush(expanded, `-${shortOption}`);
} else {
// String option in middle. Yuck.
// Expand -abfFILE to -a -b -fFILE
ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`);
break; // finished short group
}
}
ArrayPrototypeUnshiftApply(remainingArgs, expanded);
groupCount = expanded.length;
continue;
}
if (isShortOptionAndValue(arg, options)) {
// e.g. -fFILE
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
const value = StringPrototypeSlice(arg, 2);
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: `-${shortOption}`,
index, value, inlineValue: true });
continue;
}
if (isLoneLongOption(arg)) {
// e.g. '--foo'
const longOption = StringPrototypeSlice(arg, 2);
let value;
let inlineValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
isOptionValue(nextArg)) {
// e.g. '--foo', 'bar'
value = ArrayPrototypeShift(remainingArgs);
inlineValue = false;
}
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: arg,
index, value, inlineValue });
if (value != null) ++index;
continue;
}
if (isLongOptionAndValue(arg)) {
// e.g. --foo=bar
const equalIndex = StringPrototypeIndexOf(arg, '=');
const longOption = StringPrototypeSlice(arg, 2, equalIndex);
const value = StringPrototypeSlice(arg, equalIndex + 1);
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: `--${longOption}`,
index, value, inlineValue: true });
continue;
}
ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg });
}
return tokens;
}
const parseArgs = (config = kEmptyObject) => {
const args = objectGetOwn(config, 'args') ?? getMainArgs();
const strict = objectGetOwn(config, 'strict') ?? true;
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict;
const returnTokens = objectGetOwn(config, 'tokens') ?? false;
const options = objectGetOwn(config, 'options') ?? { __proto__: null };
// Bundle these up for passing to strict-mode checks.
const parseConfig = { args, strict, options, allowPositionals };
// Validate input configuration.
validateArray(args, 'args');
validateBoolean(strict, 'strict');
validateBoolean(allowPositionals, 'allowPositionals');
validateBoolean(returnTokens, 'tokens');
validateObject(options, 'options');
ArrayPrototypeForEach(
ObjectEntries(options),
({ 0: longOption, 1: optionConfig }) => {
validateObject(optionConfig, `options.${longOption}`);
// type is required
const optionType = objectGetOwn(optionConfig, 'type');
validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']);
if (ObjectHasOwn(optionConfig, 'short')) {
const shortOption = optionConfig.short;
validateString(shortOption, `options.${longOption}.short`);
if (shortOption.length !== 1) {
throw new ERR_INVALID_ARG_VALUE(
`options.${longOption}.short`,
shortOption,
'must be a single character'
);
}
}
const multipleOption = objectGetOwn(optionConfig, 'multiple');
if (ObjectHasOwn(optionConfig, 'multiple')) {
validateBoolean(multipleOption, `options.${longOption}.multiple`);
}
const defaultValue = objectGetOwn(optionConfig, 'default');
if (defaultValue !== undefined) {
let validator;
switch (optionType) {
case 'string':
validator = multipleOption ? validateStringArray : validateString;
break;
case 'boolean':
validator = multipleOption ? validateBooleanArray : validateBoolean;
break;
}
validator(defaultValue, `options.${longOption}.default`);
}
}
);
// Phase 1: identify tokens
const tokens = argsToTokens(args, options);
// Phase 2: process tokens into parsed option values and positionals
const result = {
values: { __proto__: null },
positionals: [],
};
if (returnTokens) {
result.tokens = tokens;
}
ArrayPrototypeForEach(tokens, (token) => {
if (token.kind === 'option') {
if (strict) {
checkOptionUsage(parseConfig, token);
checkOptionLikeValue(token);
}
storeOption(token.name, token.value, options, result.values);
} else if (token.kind === 'positional') {
if (!allowPositionals) {
throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value);
}
ArrayPrototypePush(result.positionals, token.value);
}
});
// Phase 3: fill in default values for missing args
ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption,
1: optionConfig }) => {
const mustSetDefault = useDefaultValueOption(longOption,
optionConfig,
result.values);
if (mustSetDefault) {
storeDefaultOption(longOption,
objectGetOwn(optionConfig, 'default'),
result.values);
}
});
return result;
};
module.exports = {
parseArgs,
};
@@ -0,0 +1,47 @@
'use strict';
class ERR_INVALID_ARG_TYPE extends TypeError {
constructor(name, expected, actual) {
super(`${name} must be ${expected} got ${actual}`);
this.code = 'ERR_INVALID_ARG_TYPE';
}
}
class ERR_INVALID_ARG_VALUE extends TypeError {
constructor(arg1, arg2, expected) {
super(`The property ${arg1} ${expected}. Received '${arg2}'`);
this.code = 'ERR_INVALID_ARG_VALUE';
}
}
class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error {
constructor(message) {
super(message);
this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE';
}
}
class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error {
constructor(option, allowPositionals) {
const suggestDashDash = allowPositionals ? `. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- ${JSON.stringify(option)}` : '';
super(`Unknown option '${option}'${suggestDashDash}`);
this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION';
}
}
class ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL extends Error {
constructor(positional) {
super(`Unexpected argument '${positional}'. This command does not take positional arguments`);
this.code = 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL';
}
}
module.exports = {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_PARSE_ARGS_INVALID_OPTION_VALUE,
ERR_PARSE_ARGS_UNKNOWN_OPTION,
ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL,
}
};
@@ -0,0 +1,393 @@
/*
This file is copied from https://github.com/nodejs/node/blob/v14.19.3/lib/internal/per_context/primordials.js
under the following license:
Copyright Node.js contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
'use strict';
/* eslint-disable node-core/prefer-primordials */
// This file subclasses and stores the JS builtins that come from the VM
// so that Node.js's builtin modules do not need to later look these up from
// the global proxy, which can be mutated by users.
// Use of primordials have sometimes a dramatic impact on performance, please
// benchmark all changes made in performance-sensitive areas of the codebase.
// See: https://github.com/nodejs/node/pull/38248
const primordials = {};
const {
defineProperty: ReflectDefineProperty,
getOwnPropertyDescriptor: ReflectGetOwnPropertyDescriptor,
ownKeys: ReflectOwnKeys,
} = Reflect;
// `uncurryThis` is equivalent to `func => Function.prototype.call.bind(func)`.
// It is using `bind.bind(call)` to avoid using `Function.prototype.bind`
// and `Function.prototype.call` after it may have been mutated by users.
const { apply, bind, call } = Function.prototype;
const uncurryThis = bind.bind(call);
primordials.uncurryThis = uncurryThis;
// `applyBind` is equivalent to `func => Function.prototype.apply.bind(func)`.
// It is using `bind.bind(apply)` to avoid using `Function.prototype.bind`
// and `Function.prototype.apply` after it may have been mutated by users.
const applyBind = bind.bind(apply);
primordials.applyBind = applyBind;
// Methods that accept a variable number of arguments, and thus it's useful to
// also create `${prefix}${key}Apply`, which uses `Function.prototype.apply`,
// instead of `Function.prototype.call`, and thus doesn't require iterator
// destructuring.
const varargsMethods = [
// 'ArrayPrototypeConcat' is omitted, because it performs the spread
// on its own for arrays and array-likes with a truthy
// @@isConcatSpreadable symbol property.
'ArrayOf',
'ArrayPrototypePush',
'ArrayPrototypeUnshift',
// 'FunctionPrototypeCall' is omitted, since there's 'ReflectApply'
// and 'FunctionPrototypeApply'.
'MathHypot',
'MathMax',
'MathMin',
'StringPrototypeConcat',
'TypedArrayOf',
];
function getNewKey(key) {
return typeof key === 'symbol' ?
`Symbol${key.description[7].toUpperCase()}${key.description.slice(8)}` :
`${key[0].toUpperCase()}${key.slice(1)}`;
}
function copyAccessor(dest, prefix, key, { enumerable, get, set }) {
ReflectDefineProperty(dest, `${prefix}Get${key}`, {
value: uncurryThis(get),
enumerable
});
if (set !== undefined) {
ReflectDefineProperty(dest, `${prefix}Set${key}`, {
value: uncurryThis(set),
enumerable
});
}
}
function copyPropsRenamed(src, dest, prefix) {
for (const key of ReflectOwnKeys(src)) {
const newKey = getNewKey(key);
const desc = ReflectGetOwnPropertyDescriptor(src, key);
if ('get' in desc) {
copyAccessor(dest, prefix, newKey, desc);
} else {
const name = `${prefix}${newKey}`;
ReflectDefineProperty(dest, name, desc);
if (varargsMethods.includes(name)) {
ReflectDefineProperty(dest, `${name}Apply`, {
// `src` is bound as the `this` so that the static `this` points
// to the object it was defined on,
// e.g.: `ArrayOfApply` gets a `this` of `Array`:
value: applyBind(desc.value, src),
});
}
}
}
}
function copyPropsRenamedBound(src, dest, prefix) {
for (const key of ReflectOwnKeys(src)) {
const newKey = getNewKey(key);
const desc = ReflectGetOwnPropertyDescriptor(src, key);
if ('get' in desc) {
copyAccessor(dest, prefix, newKey, desc);
} else {
const { value } = desc;
if (typeof value === 'function') {
desc.value = value.bind(src);
}
const name = `${prefix}${newKey}`;
ReflectDefineProperty(dest, name, desc);
if (varargsMethods.includes(name)) {
ReflectDefineProperty(dest, `${name}Apply`, {
value: applyBind(value, src),
});
}
}
}
}
function copyPrototype(src, dest, prefix) {
for (const key of ReflectOwnKeys(src)) {
const newKey = getNewKey(key);
const desc = ReflectGetOwnPropertyDescriptor(src, key);
if ('get' in desc) {
copyAccessor(dest, prefix, newKey, desc);
} else {
const { value } = desc;
if (typeof value === 'function') {
desc.value = uncurryThis(value);
}
const name = `${prefix}${newKey}`;
ReflectDefineProperty(dest, name, desc);
if (varargsMethods.includes(name)) {
ReflectDefineProperty(dest, `${name}Apply`, {
value: applyBind(value),
});
}
}
}
}
// Create copies of configurable value properties of the global object
[
'Proxy',
'globalThis',
].forEach((name) => {
// eslint-disable-next-line no-restricted-globals
primordials[name] = globalThis[name];
});
// Create copies of URI handling functions
[
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
].forEach((fn) => {
primordials[fn.name] = fn;
});
// Create copies of the namespace objects
[
'JSON',
'Math',
'Proxy',
'Reflect',
].forEach((name) => {
// eslint-disable-next-line no-restricted-globals
copyPropsRenamed(global[name], primordials, name);
});
// Create copies of intrinsic objects
[
'Array',
'ArrayBuffer',
'BigInt',
'BigInt64Array',
'BigUint64Array',
'Boolean',
'DataView',
'Date',
'Error',
'EvalError',
'Float32Array',
'Float64Array',
'Function',
'Int16Array',
'Int32Array',
'Int8Array',
'Map',
'Number',
'Object',
'RangeError',
'ReferenceError',
'RegExp',
'Set',
'String',
'Symbol',
'SyntaxError',
'TypeError',
'URIError',
'Uint16Array',
'Uint32Array',
'Uint8Array',
'Uint8ClampedArray',
'WeakMap',
'WeakSet',
].forEach((name) => {
// eslint-disable-next-line no-restricted-globals
const original = global[name];
primordials[name] = original;
copyPropsRenamed(original, primordials, name);
copyPrototype(original.prototype, primordials, `${name}Prototype`);
});
// Create copies of intrinsic objects that require a valid `this` to call
// static methods.
// Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all
[
'Promise',
].forEach((name) => {
// eslint-disable-next-line no-restricted-globals
const original = global[name];
primordials[name] = original;
copyPropsRenamedBound(original, primordials, name);
copyPrototype(original.prototype, primordials, `${name}Prototype`);
});
// Create copies of abstract intrinsic objects that are not directly exposed
// on the global object.
// Refs: https://tc39.es/ecma262/#sec-%typedarray%-intrinsic-object
[
{ name: 'TypedArray', original: Reflect.getPrototypeOf(Uint8Array) },
{ name: 'ArrayIterator', original: {
prototype: Reflect.getPrototypeOf(Array.prototype[Symbol.iterator]()),
} },
{ name: 'StringIterator', original: {
prototype: Reflect.getPrototypeOf(String.prototype[Symbol.iterator]()),
} },
].forEach(({ name, original }) => {
primordials[name] = original;
// The static %TypedArray% methods require a valid `this`, but can't be bound,
// as they need a subclass constructor as the receiver:
copyPrototype(original, primordials, name);
copyPrototype(original.prototype, primordials, `${name}Prototype`);
});
/* eslint-enable node-core/prefer-primordials */
const {
ArrayPrototypeForEach,
FunctionPrototypeCall,
Map,
ObjectFreeze,
ObjectSetPrototypeOf,
Set,
SymbolIterator,
WeakMap,
WeakSet,
} = primordials;
// Because these functions are used by `makeSafe`, which is exposed
// on the `primordials` object, it's important to use const references
// to the primordials that they use:
const createSafeIterator = (factory, next) => {
class SafeIterator {
constructor(iterable) {
this._iterator = factory(iterable);
}
next() {
return next(this._iterator);
}
[SymbolIterator]() {
return this;
}
}
ObjectSetPrototypeOf(SafeIterator.prototype, null);
ObjectFreeze(SafeIterator.prototype);
ObjectFreeze(SafeIterator);
return SafeIterator;
};
primordials.SafeArrayIterator = createSafeIterator(
primordials.ArrayPrototypeSymbolIterator,
primordials.ArrayIteratorPrototypeNext
);
primordials.SafeStringIterator = createSafeIterator(
primordials.StringPrototypeSymbolIterator,
primordials.StringIteratorPrototypeNext
);
const copyProps = (src, dest) => {
ArrayPrototypeForEach(ReflectOwnKeys(src), (key) => {
if (!ReflectGetOwnPropertyDescriptor(dest, key)) {
ReflectDefineProperty(
dest,
key,
ReflectGetOwnPropertyDescriptor(src, key));
}
});
};
const makeSafe = (unsafe, safe) => {
if (SymbolIterator in unsafe.prototype) {
const dummy = new unsafe();
let next; // We can reuse the same `next` method.
ArrayPrototypeForEach(ReflectOwnKeys(unsafe.prototype), (key) => {
if (!ReflectGetOwnPropertyDescriptor(safe.prototype, key)) {
const desc = ReflectGetOwnPropertyDescriptor(unsafe.prototype, key);
if (
typeof desc.value === 'function' &&
desc.value.length === 0 &&
SymbolIterator in (FunctionPrototypeCall(desc.value, dummy) ?? {})
) {
const createIterator = uncurryThis(desc.value);
next = next ?? uncurryThis(createIterator(dummy).next);
const SafeIterator = createSafeIterator(createIterator, next);
desc.value = function() {
return new SafeIterator(this);
};
}
ReflectDefineProperty(safe.prototype, key, desc);
}
});
} else {
copyProps(unsafe.prototype, safe.prototype);
}
copyProps(unsafe, safe);
ObjectSetPrototypeOf(safe.prototype, null);
ObjectFreeze(safe.prototype);
ObjectFreeze(safe);
return safe;
};
primordials.makeSafe = makeSafe;
// Subclass the constructors because we need to use their prototype
// methods later.
// Defining the `constructor` is necessary here to avoid the default
// constructor which uses the user-mutable `%ArrayIteratorPrototype%.next`.
primordials.SafeMap = makeSafe(
Map,
class SafeMap extends Map {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
}
);
primordials.SafeWeakMap = makeSafe(
WeakMap,
class SafeWeakMap extends WeakMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
}
);
primordials.SafeSet = makeSafe(
Set,
class SafeSet extends Set {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
}
);
primordials.SafeWeakSet = makeSafe(
WeakSet,
class SafeWeakSet extends WeakSet {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
}
);
ObjectSetPrototypeOf(primordials, null);
ObjectFreeze(primordials);
module.exports = primordials;
@@ -0,0 +1,14 @@
'use strict';
// This is a placeholder for util.js in node.js land.
const {
ObjectCreate,
ObjectFreeze,
} = require('./primordials');
const kEmptyObject = ObjectFreeze(ObjectCreate(null));
module.exports = {
kEmptyObject,
};
@@ -0,0 +1,89 @@
'use strict';
// This file is a proxy of the original file located at:
// https://github.com/nodejs/node/blob/main/lib/internal/validators.js
// Every addition or modification to this file must be evaluated
// during the PR review.
const {
ArrayIsArray,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
} = require('./primordials');
const {
codes: {
ERR_INVALID_ARG_TYPE
}
} = require('./errors');
function validateString(value, name) {
if (typeof value !== 'string') {
throw new ERR_INVALID_ARG_TYPE(name, 'String', value);
}
}
function validateUnion(value, name, union) {
if (!ArrayPrototypeIncludes(union, value)) {
throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value);
}
}
function validateBoolean(value, name) {
if (typeof value !== 'boolean') {
throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value);
}
}
function validateArray(value, name) {
if (!ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
}
}
function validateStringArray(value, name) {
validateArray(value, name);
for (let i = 0; i < value.length; i++) {
validateString(value[i], `${name}[${i}]`);
}
}
function validateBooleanArray(value, name) {
validateArray(value, name);
for (let i = 0; i < value.length; i++) {
validateBoolean(value[i], `${name}[${i}]`);
}
}
/**
* @param {unknown} value
* @param {string} name
* @param {{
* allowArray?: boolean,
* allowFunction?: boolean,
* nullable?: boolean
* }} [options]
*/
function validateObject(value, name, options) {
const useDefaultOptions = options == null;
const allowArray = useDefaultOptions ? false : options.allowArray;
const allowFunction = useDefaultOptions ? false : options.allowFunction;
const nullable = useDefaultOptions ? false : options.nullable;
if ((!nullable && value === null) ||
(!allowArray && ArrayIsArray(value)) ||
(typeof value !== 'object' && (
!allowFunction || typeof value !== 'function'
))) {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
}
module.exports = {
validateArray,
validateObject,
validateString,
validateStringArray,
validateUnion,
validateBoolean,
validateBooleanArray,
};
@@ -0,0 +1,36 @@
{
"name": "@pkgjs/parseargs",
"version": "0.11.0",
"description": "Polyfill of future proposal for `util.parseArgs()`",
"engines": {
"node": ">=14"
},
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"scripts": {
"coverage": "c8 --check-coverage tape 'test/*.js'",
"test": "c8 tape 'test/*.js'",
"posttest": "eslint .",
"fix": "npm run posttest -- --fix"
},
"repository": {
"type": "git",
"url": "git@github.com:pkgjs/parseargs.git"
},
"keywords": [],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/pkgjs/parseargs/issues"
},
"homepage": "https://github.com/pkgjs/parseargs#readme",
"devDependencies": {
"c8": "^7.10.0",
"eslint": "^8.2.0",
"eslint-plugin-node-core": "iansu/eslint-plugin-node-core",
"tape": "^5.2.2"
}
}
@@ -0,0 +1,198 @@
'use strict';
const {
ArrayPrototypeFind,
ObjectEntries,
ObjectPrototypeHasOwnProperty: ObjectHasOwn,
StringPrototypeCharAt,
StringPrototypeIncludes,
StringPrototypeStartsWith,
} = require('./internal/primordials');
const {
validateObject,
} = require('./internal/validators');
// These are internal utilities to make the parsing logic easier to read, and
// add lots of detail for the curious. They are in a separate file to allow
// unit testing, although that is not essential (this could be rolled into
// main file and just tested implicitly via API).
//
// These routines are for internal use, not for export to client.
/**
* Return the named property, but only if it is an own property.
*/
function objectGetOwn(obj, prop) {
if (ObjectHasOwn(obj, prop))
return obj[prop];
}
/**
* Return the named options property, but only if it is an own property.
*/
function optionsGetOwn(options, longOption, prop) {
if (ObjectHasOwn(options, longOption))
return objectGetOwn(options[longOption], prop);
}
/**
* Determines if the argument may be used as an option value.
* @example
* isOptionValue('V') // returns true
* isOptionValue('-v') // returns true (greedy)
* isOptionValue('--foo') // returns true (greedy)
* isOptionValue(undefined) // returns false
*/
function isOptionValue(value) {
if (value == null) return false;
// Open Group Utility Conventions are that an option-argument
// is the argument after the option, and may start with a dash.
return true; // greedy!
}
/**
* Detect whether there is possible confusion and user may have omitted
* the option argument, like `--port --verbose` when `port` of type:string.
* In strict mode we throw errors if value is option-like.
*/
function isOptionLikeValue(value) {
if (value == null) return false;
return value.length > 1 && StringPrototypeCharAt(value, 0) === '-';
}
/**
* Determines if `arg` is just a short option.
* @example '-f'
*/
function isLoneShortOption(arg) {
return arg.length === 2 &&
StringPrototypeCharAt(arg, 0) === '-' &&
StringPrototypeCharAt(arg, 1) !== '-';
}
/**
* Determines if `arg` is a lone long option.
* @example
* isLoneLongOption('a') // returns false
* isLoneLongOption('-a') // returns false
* isLoneLongOption('--foo') // returns true
* isLoneLongOption('--foo=bar') // returns false
*/
function isLoneLongOption(arg) {
return arg.length > 2 &&
StringPrototypeStartsWith(arg, '--') &&
!StringPrototypeIncludes(arg, '=', 3);
}
/**
* Determines if `arg` is a long option and value in the same argument.
* @example
* isLongOptionAndValue('--foo') // returns false
* isLongOptionAndValue('--foo=bar') // returns true
*/
function isLongOptionAndValue(arg) {
return arg.length > 2 &&
StringPrototypeStartsWith(arg, '--') &&
StringPrototypeIncludes(arg, '=', 3);
}
/**
* Determines if `arg` is a short option group.
*
* See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html).
* One or more options without option-arguments, followed by at most one
* option that takes an option-argument, should be accepted when grouped
* behind one '-' delimiter.
* @example
* isShortOptionGroup('-a', {}) // returns false
* isShortOptionGroup('-ab', {}) // returns true
* // -fb is an option and a value, not a short option group
* isShortOptionGroup('-fb', {
* options: { f: { type: 'string' } }
* }) // returns false
* isShortOptionGroup('-bf', {
* options: { f: { type: 'string' } }
* }) // returns true
* // -bfb is an edge case, return true and caller sorts it out
* isShortOptionGroup('-bfb', {
* options: { f: { type: 'string' } }
* }) // returns true
*/
function isShortOptionGroup(arg, options) {
if (arg.length <= 2) return false;
if (StringPrototypeCharAt(arg, 0) !== '-') return false;
if (StringPrototypeCharAt(arg, 1) === '-') return false;
const firstShort = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(firstShort, options);
return optionsGetOwn(options, longOption, 'type') !== 'string';
}
/**
* Determine if arg is a short string option followed by its value.
* @example
* isShortOptionAndValue('-a', {}); // returns false
* isShortOptionAndValue('-ab', {}); // returns false
* isShortOptionAndValue('-fFILE', {
* options: { foo: { short: 'f', type: 'string' }}
* }) // returns true
*/
function isShortOptionAndValue(arg, options) {
validateObject(options, 'options');
if (arg.length <= 2) return false;
if (StringPrototypeCharAt(arg, 0) !== '-') return false;
if (StringPrototypeCharAt(arg, 1) === '-') return false;
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
return optionsGetOwn(options, longOption, 'type') === 'string';
}
/**
* Find the long option associated with a short option. Looks for a configured
* `short` and returns the short option itself if a long option is not found.
* @example
* findLongOptionForShort('a', {}) // returns 'a'
* findLongOptionForShort('b', {
* options: { bar: { short: 'b' } }
* }) // returns 'bar'
*/
function findLongOptionForShort(shortOption, options) {
validateObject(options, 'options');
const longOptionEntry = ArrayPrototypeFind(
ObjectEntries(options),
({ 1: optionConfig }) => objectGetOwn(optionConfig, 'short') === shortOption
);
return longOptionEntry?.[0] ?? shortOption;
}
/**
* Check if the given option includes a default value
* and that option has not been set by the input args.
*
* @param {string} longOption - long option name e.g. 'foo'
* @param {object} optionConfig - the option configuration properties
* @param {object} values - option values returned in `values` by parseArgs
*/
function useDefaultValueOption(longOption, optionConfig, values) {
return objectGetOwn(optionConfig, 'default') !== undefined &&
values[longOption] === undefined;
}
module.exports = {
findLongOptionForShort,
isLoneLongOption,
isLoneShortOption,
isLongOptionAndValue,
isOptionValue,
isOptionLikeValue,
isShortOptionAndValue,
isShortOptionGroup,
useDefaultValueOption,
objectGetOwn,
optionsGetOwn,
};
@@ -0,0 +1,3 @@
# `@rolldown/binding-darwin-arm64`
This is the **aarch64-apple-darwin** binary for `@rolldown/binding`
@@ -0,0 +1,37 @@
{
"name": "@rolldown/binding-darwin-arm64",
"version": "1.2.6",
"cpu": [
"arm64"
],
"main": "rolldown-binding.darwin-arm64.node",
"files": [
"rolldown-binding.darwin-arm64.node"
],
"description": "Fast JavaScript/TypeScript bundler in Rust with Rollup-compatible API.",
"keywords": [
"bundler",
"esbuild",
"parcel",
"rolldown",
"rollup",
"webpack"
],
"homepage": "https://rolldown.rs/",
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/rolldown/rolldown.git",
"directory": "packages/rolldown"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"os": [
"darwin"
]
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026-present, rolldown/plugins repository contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,145 @@
# @rolldown/pluginutils [![npm](https://img.shields.io/npm/v/@rolldown/pluginutils.svg)](https://npmx.dev/package/@rolldown/pluginutils)
Plugin utilities for [Rolldown](https://rolldown.rs).
Includes regex helpers for plugin hook filters, composable filter expressions, and a helper for filtering out Vite-serve-only plugins.
## Install
```bash
pnpm add -D @rolldown/pluginutils
```
## Usage
```ts
import { exactRegex, prefixRegex, makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils'
```
All filter helpers are also exposed via the `/filter` subpath:
```ts
import { and, or, id, include } from '@rolldown/pluginutils/filter'
```
## Regex helpers
### `exactRegex`
- **Type:** `(str: string, flags?: string) => RegExp`
Constructs a `RegExp` that matches the exact string specified. Useful as a plugin hook filter.
```ts
import { exactRegex } from '@rolldown/pluginutils'
const plugin = {
name: 'plugin',
resolveId: {
filter: { id: exactRegex('foo') },
handler(id) {}, // only called for `foo`
},
}
```
### `prefixRegex`
- **Type:** `(str: string, flags?: string) => RegExp`
Constructs a `RegExp` that matches values starting with the specified prefix.
```ts
import { prefixRegex } from '@rolldown/pluginutils'
const plugin = {
name: 'plugin',
resolveId: {
filter: { id: prefixRegex('foo') },
handler(id) {}, // called for IDs starting with `foo`
},
}
```
### `makeIdFiltersToMatchWithQuery`
- **Type:** `(input: string | RegExp | (string | RegExp)[]) => string | RegExp | (string | RegExp)[]`
Converts an id filter so that it also matches ids that include a query string.
```ts
import { makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils'
const plugin = {
name: 'plugin',
transform: {
filter: { id: makeIdFiltersToMatchWithQuery(['**/*.js', /\.ts$/]) },
// Matches:
// foo.js, foo.js?foo, foo.txt?foo.js,
// foo.ts, foo.ts?foo, foo.txt?foo.ts
handler(code, id) {},
},
}
```
## Composable filters
[Composable filter expressions](https://rolldown.rs/apis/plugin-api/hook-filters#composable-filters) for use cases where a simple `id`/`include`/`exclude` is not enough. For example, when a plugin needs to combine `id`, `moduleType`, `code`, and `query` conditions.
```ts
import { and, code, id, include, interpreter, moduleType, or } from '@rolldown/pluginutils'
const expr = include(and(or(id(/\.tsx?$/), id(/\.jsx?$/)), moduleType('tsx'), code(/import React/)))
interpreter(expr, sourceCode, sourceId, 'tsx') // boolean
```
### Builders
| Builder | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `and(...exprs)` | All operands must match. |
| `or(...exprs)` | At least one operand must match. |
| `not(expr)` | Negates the operand. |
| `id(pattern, params?)` | Match the module id. `pattern` is `string` or `RegExp`. `params.cleanUrl` strips the query/hash before matching. |
| `importerId(pattern, params?)` | Match the importer's id. Same shape as `id`. |
| `moduleType(type)` | Match Rolldown's module type (`'js'`, `'jsx'`, `'ts'`, `'tsx'`, `'json'`, `'text'`, `'base64'`, `'dataurl'`, `'binary'`, `'empty'`, or a custom string). |
| `code(pattern)` | Match the module source. `string` matches with `includes`; `RegExp` with `test`. |
| `query(key, pattern)` | Match a single query parameter. `pattern` is `boolean` (key presence/truthiness), `string` (exact value), or `RegExp` (value pattern). |
| `queries(obj)` | Shorthand for `and(...)` over multiple `query` entries. |
| `include(expr)` | Top-level wrapper marking `expr` as an inclusion rule. |
| `exclude(expr)` | Top-level wrapper marking `expr` as an exclusion rule. |
### `interpreter`
- **Type:** `(exprs, code?, id?, moduleType?, importerId?) => boolean`
Evaluates one or more top-level expressions against the given inputs. Returns `true` when at least one `include` matches and no `exclude` matches; when no `include` is present, defaults to `true` unless an `exclude` matches.
The argument required by each expression must be provided. For example, evaluating an `id(...)` expression without passing `id` will throw.
## `filterVitePlugins`
- **Type:** `<T>(plugins: T | T[] | null | undefined | false) => T[]`
Removes Vite plugins that target the dev server (`apply: 'serve'`) from a (possibly nested) plugin array. Plugins whose `apply` is a function are invoked with a `command: 'build'` context to decide. Useful when reusing a Vite plugin array inside a Rolldown config.
```ts
import { defineConfig } from 'rolldown'
import { filterVitePlugins } from '@rolldown/pluginutils'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: filterVitePlugins([
viteReact(),
{
name: 'dev-only',
apply: 'serve', // filtered out
// ...
},
]),
})
```
## License
MIT
@@ -0,0 +1,323 @@
//#region src/utils.ts
const postfixRE = /[?#].*$/;
function cleanUrl(url) {
return url.replace(postfixRE, "");
}
function extractQueryWithoutFragment(url) {
const questionMarkIndex = url.indexOf("?");
if (questionMarkIndex === -1) return "";
const fragmentIndex = url.indexOf("#", questionMarkIndex);
if (fragmentIndex === -1) return url.substring(questionMarkIndex);
else return url.substring(questionMarkIndex, fragmentIndex);
}
//#endregion
//#region src/filter/composable-filters.ts
var And = class {
kind;
args;
constructor(...args) {
if (args.length === 0) throw new Error("`And` expects at least one operand");
this.args = args;
this.kind = "and";
}
};
var Or = class {
kind;
args;
constructor(...args) {
if (args.length === 0) throw new Error("`Or` expects at least one operand");
this.args = args;
this.kind = "or";
}
};
var Not = class {
kind;
expr;
constructor(expr) {
this.expr = expr;
this.kind = "not";
}
};
var Id = class {
kind;
pattern;
params;
constructor(pattern, params) {
this.pattern = pattern;
this.kind = "id";
this.params = params ?? { cleanUrl: false };
}
};
var ImporterId = class {
kind;
pattern;
params;
constructor(pattern, params) {
this.pattern = pattern;
this.kind = "importerId";
this.params = params ?? { cleanUrl: false };
}
};
var ModuleType = class {
kind;
pattern;
constructor(pattern) {
this.pattern = pattern;
this.kind = "moduleType";
}
};
var Code = class {
kind;
pattern;
constructor(expr) {
this.pattern = expr;
this.kind = "code";
}
};
var Query = class {
kind;
key;
pattern;
constructor(key, pattern) {
this.pattern = pattern;
this.key = key;
this.kind = "query";
}
};
var Include = class {
kind;
expr;
constructor(expr) {
this.expr = expr;
this.kind = "include";
}
};
var Exclude = class {
kind;
expr;
constructor(expr) {
this.expr = expr;
this.kind = "exclude";
}
};
function and(...args) {
return new And(...args);
}
function or(...args) {
return new Or(...args);
}
function not(expr) {
return new Not(expr);
}
function id(pattern, params) {
return new Id(pattern, params);
}
function importerId(pattern, params) {
return new ImporterId(pattern, params);
}
function moduleType(pattern) {
return new ModuleType(pattern);
}
function code(pattern) {
return new Code(pattern);
}
function query(key, pattern) {
return new Query(key, pattern);
}
function include(expr) {
return new Include(expr);
}
function exclude(expr) {
return new Exclude(expr);
}
/**
* convert a queryObject to FilterExpression like
* ```js
* and(query(k1, v1), query(k2, v2))
* ```
* @param queryFilterObject The query filter object needs to be matched.
* @returns a `And` FilterExpression
*/
function queries(queryFilter) {
return and(...Object.entries(queryFilter).map(([key, value]) => {
return new Query(key, value);
}));
}
function interpreter(exprs, code, id, moduleType, importerId) {
let arr = [];
if (Array.isArray(exprs)) arr = exprs;
else arr = [exprs];
return interpreterImpl(arr, code, id, moduleType, importerId);
}
function interpreterImpl(expr, code, id, moduleType, importerId, ctx = {}) {
let hasInclude = false;
for (const e of expr) switch (e.kind) {
case "include":
hasInclude = true;
if (exprInterpreter(e.expr, code, id, moduleType, importerId, ctx)) return true;
break;
case "exclude":
if (exprInterpreter(e.expr, code, id, moduleType, importerId, ctx)) return false;
break;
}
return !hasInclude;
}
function exprInterpreter(expr, code, id, moduleType, importerId, ctx = {}) {
switch (expr.kind) {
case "and": return expr.args.every((e) => exprInterpreter(e, code, id, moduleType, importerId, ctx));
case "or": return expr.args.some((e) => exprInterpreter(e, code, id, moduleType, importerId, ctx));
case "not": return !exprInterpreter(expr.expr, code, id, moduleType, importerId, ctx);
case "id": {
if (id === void 0) throw new Error("`id` is required for `id` expression");
let idToMatch = id;
if (expr.params.cleanUrl) idToMatch = cleanUrl(idToMatch);
return typeof expr.pattern === "string" ? idToMatch === expr.pattern : expr.pattern.test(idToMatch);
}
case "importerId": {
if (importerId === void 0) return false;
let importerIdToMatch = importerId;
if (expr.params.cleanUrl) importerIdToMatch = cleanUrl(importerIdToMatch);
return typeof expr.pattern === "string" ? importerIdToMatch === expr.pattern : expr.pattern.test(importerIdToMatch);
}
case "moduleType":
if (moduleType === void 0) throw new Error("`moduleType` is required for `moduleType` expression");
return moduleType === expr.pattern;
case "code":
if (code === void 0) throw new Error("`code` is required for `code` expression");
return typeof expr.pattern === "string" ? code.includes(expr.pattern) : expr.pattern.test(code);
case "query": {
if (id === void 0) throw new Error("`id` is required for `Query` expression");
if (!ctx.urlSearchParamsCache) {
let queryString = extractQueryWithoutFragment(id);
ctx.urlSearchParamsCache = new URLSearchParams(queryString);
}
let urlParams = ctx.urlSearchParamsCache;
if (typeof expr.pattern === "boolean") if (expr.pattern) return urlParams.has(expr.key);
else return !urlParams.has(expr.key);
else if (typeof expr.pattern === "string") return urlParams.get(expr.key) === expr.pattern;
else return expr.pattern.test(urlParams.get(expr.key) ?? "");
}
default: throw new Error(`Expression ${JSON.stringify(expr)} is not expected.`);
}
}
//#endregion
//#region src/filter/filter-vite-plugins.ts
/**
* Filters out Vite plugins that have `apply: 'serve'` set.
*
* Since Rolldown operates in build mode, plugins marked with `apply: 'serve'`
* are intended only for Vite's dev server and should be excluded from the build process.
*
* @param plugins - Array of plugins (can include nested arrays)
* @returns Filtered array with serve-only plugins removed
*
* @example
* ```ts
* import { defineConfig } from 'rolldown';
* import { filterVitePlugins } from '@rolldown/pluginutils';
* import viteReact from '@vitejs/plugin-react';
*
* export default defineConfig({
* plugins: filterVitePlugins([
* viteReact(),
* {
* name: 'dev-only',
* apply: 'serve', // This will be filtered out
* // ...
* }
* ])
* });
* ```
*/
function filterVitePlugins(plugins) {
if (!plugins) return [];
const pluginArray = Array.isArray(plugins) ? plugins : [plugins];
const result = [];
for (const plugin of pluginArray) {
if (!plugin) continue;
if (Array.isArray(plugin)) {
result.push(...filterVitePlugins(plugin));
continue;
}
const pluginWithApply = plugin;
if ("apply" in pluginWithApply) {
const applyValue = pluginWithApply.apply;
if (typeof applyValue === "function") try {
if (applyValue({}, {
command: "build",
mode: "production"
})) result.push(plugin);
} catch {
result.push(plugin);
}
else if (applyValue === "serve") continue;
else result.push(plugin);
} else result.push(plugin);
}
return result;
}
//#endregion
//#region src/filter/simple-filters.ts
/**
* Constructs a RegExp that matches the exact string specified.
*
* This is useful for plugin hook filters.
*
* @param str the string to match.
* @param flags flags for the RegExp.
*
* @example
* ```ts
* import { exactRegex } from '@rolldown/pluginutils';
* const plugin = {
* name: 'plugin',
* resolveId: {
* filter: { id: exactRegex('foo') },
* handler(id) {} // will only be called for `foo`
* }
* }
* ```
*/
function exactRegex(str, flags) {
return new RegExp(`^${escapeRegex(str)}$`, flags);
}
/**
* Constructs a RegExp that matches a value that has the specified prefix.
*
* This is useful for plugin hook filters.
*
* @param str the string to match.
* @param flags flags for the RegExp.
*
* @example
* ```ts
* import { prefixRegex } from '@rolldown/pluginutils';
* const plugin = {
* name: 'plugin',
* resolveId: {
* filter: { id: prefixRegex('foo') },
* handler(id) {} // will only be called for IDs starting with `foo`
* }
* }
* ```
*/
function prefixRegex(str, flags) {
return new RegExp(`^${escapeRegex(str)}`, flags);
}
const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g;
function escapeRegex(str) {
return str.replace(escapeRegexRE, "\\$&");
}
function makeIdFiltersToMatchWithQuery(input) {
if (!Array.isArray(input)) return makeIdFilterToMatchWithQuery(input);
return input.map((i) => makeIdFilterToMatchWithQuery(i));
}
function makeIdFilterToMatchWithQuery(input) {
if (typeof input === "string") return `${input}{?*,}`;
return makeRegexIdFilterToMatchWithQuery(input);
}
function makeRegexIdFilterToMatchWithQuery(input) {
return new RegExp(input.source.replace(/(?<!\\)\$/g, "(?:\\?.*)?$"), input.flags);
}
//#endregion
export { queries as _, and as a, exprInterpreter as c, include as d, interpreter as f, or as g, not as h, filterVitePlugins as i, id as l, moduleType as m, makeIdFiltersToMatchWithQuery as n, code as o, interpreterImpl as p, prefixRegex as r, exclude as s, exactRegex as t, importerId as u, query as v };
@@ -0,0 +1,194 @@
//#region src/filter/composable-filters.d.ts
type StringOrRegExp = string | RegExp;
type PluginModuleType = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'text' | 'base64' | 'dataurl' | 'binary' | 'empty' | (string & {});
type FilterExpressionKind = FilterExpression['kind'];
type FilterExpression = And | Or | Not | Id | ImporterId | ModuleType | Code | Query;
type TopLevelFilterExpression = Include | Exclude;
declare class And {
kind: 'and';
args: FilterExpression[];
constructor(...args: FilterExpression[]);
}
declare class Or {
kind: 'or';
args: FilterExpression[];
constructor(...args: FilterExpression[]);
}
declare class Not {
kind: 'not';
expr: FilterExpression;
constructor(expr: FilterExpression);
}
interface QueryFilterObject {
[key: string]: StringOrRegExp | boolean;
}
interface IdParams {
cleanUrl?: boolean;
}
declare class Id {
kind: 'id';
pattern: StringOrRegExp;
params: IdParams;
constructor(pattern: StringOrRegExp, params?: IdParams);
}
declare class ImporterId {
kind: 'importerId';
pattern: StringOrRegExp;
params: IdParams;
constructor(pattern: StringOrRegExp, params?: IdParams);
}
declare class ModuleType {
kind: 'moduleType';
pattern: PluginModuleType;
constructor(pattern: PluginModuleType);
}
declare class Code {
kind: 'code';
pattern: StringOrRegExp;
constructor(expr: StringOrRegExp);
}
declare class Query {
kind: 'query';
key: string;
pattern: StringOrRegExp | boolean;
constructor(key: string, pattern: StringOrRegExp | boolean);
}
declare class Include {
kind: 'include';
expr: FilterExpression;
constructor(expr: FilterExpression);
}
declare class Exclude {
kind: 'exclude';
expr: FilterExpression;
constructor(expr: FilterExpression);
}
declare function and(...args: FilterExpression[]): And;
declare function or(...args: FilterExpression[]): Or;
declare function not(expr: FilterExpression): Not;
declare function id(pattern: StringOrRegExp, params?: IdParams): Id;
declare function importerId(pattern: StringOrRegExp, params?: IdParams): ImporterId;
declare function moduleType(pattern: PluginModuleType): ModuleType;
declare function code(pattern: StringOrRegExp): Code;
declare function query(key: string, pattern: StringOrRegExp | boolean): Query;
declare function include(expr: FilterExpression): Include;
declare function exclude(expr: FilterExpression): Exclude;
/**
* convert a queryObject to FilterExpression like
* ```js
* and(query(k1, v1), query(k2, v2))
* ```
* @param queryFilterObject The query filter object needs to be matched.
* @returns a `And` FilterExpression
*/
declare function queries(queryFilter: QueryFilterObject): And;
declare function interpreter(exprs: TopLevelFilterExpression | TopLevelFilterExpression[], code?: string, id?: string, moduleType?: PluginModuleType, importerId?: string): boolean;
interface InterpreterCtx {
urlSearchParamsCache?: URLSearchParams;
}
declare function interpreterImpl(expr: TopLevelFilterExpression[], code?: string, id?: string, moduleType?: PluginModuleType, importerId?: string, ctx?: InterpreterCtx): boolean;
declare function exprInterpreter(expr: FilterExpression, code?: string, id?: string, moduleType?: PluginModuleType, importerId?: string, ctx?: InterpreterCtx): boolean;
//#endregion
//#region src/filter/filter-vite-plugins.d.ts
/**
* Filters out Vite plugins that have `apply: 'serve'` set.
*
* Since Rolldown operates in build mode, plugins marked with `apply: 'serve'`
* are intended only for Vite's dev server and should be excluded from the build process.
*
* @param plugins - Array of plugins (can include nested arrays)
* @returns Filtered array with serve-only plugins removed
*
* @example
* ```ts
* import { defineConfig } from 'rolldown';
* import { filterVitePlugins } from '@rolldown/pluginutils';
* import viteReact from '@vitejs/plugin-react';
*
* export default defineConfig({
* plugins: filterVitePlugins([
* viteReact(),
* {
* name: 'dev-only',
* apply: 'serve', // This will be filtered out
* // ...
* }
* ])
* });
* ```
*/
declare function filterVitePlugins<T = any>(plugins: T | T[] | null | undefined | false): T[];
//#endregion
//#region src/filter/simple-filters.d.ts
/**
* Constructs a RegExp that matches the exact string specified.
*
* This is useful for plugin hook filters.
*
* @param str the string to match.
* @param flags flags for the RegExp.
*
* @example
* ```ts
* import { exactRegex } from '@rolldown/pluginutils';
* const plugin = {
* name: 'plugin',
* resolveId: {
* filter: { id: exactRegex('foo') },
* handler(id) {} // will only be called for `foo`
* }
* }
* ```
*/
declare function exactRegex(str: string, flags?: string): RegExp;
/**
* Constructs a RegExp that matches a value that has the specified prefix.
*
* This is useful for plugin hook filters.
*
* @param str the string to match.
* @param flags flags for the RegExp.
*
* @example
* ```ts
* import { prefixRegex } from '@rolldown/pluginutils';
* const plugin = {
* name: 'plugin',
* resolveId: {
* filter: { id: prefixRegex('foo') },
* handler(id) {} // will only be called for IDs starting with `foo`
* }
* }
* ```
*/
declare function prefixRegex(str: string, flags?: string): RegExp;
type WidenString<T> = T extends string ? string : T;
/**
* Converts a id filter to match with an id with a query.
*
* @param input the id filters to convert.
*
* @example
* ```ts
* import { makeIdFiltersToMatchWithQuery } from '@rolldown/pluginutils';
* const plugin = {
* name: 'plugin',
* transform: {
* filter: { id: makeIdFiltersToMatchWithQuery(['**' + '/*.js', /\.ts$/]) },
* // The handler will be called for IDs like:
* // - foo.js
* // - foo.js?foo
* // - foo.txt?foo.js
* // - foo.ts
* // - foo.ts?foo
* // - foo.txt?foo.ts
* handler(code, id) {}
* }
* }
* ```
*/
declare function makeIdFiltersToMatchWithQuery<T extends string | RegExp>(input: T): WidenString<T>;
declare function makeIdFiltersToMatchWithQuery<T extends string | RegExp>(input: readonly T[]): WidenString<T>[];
declare function makeIdFiltersToMatchWithQuery(input: string | RegExp | readonly (string | RegExp)[]): string | RegExp | (string | RegExp)[];
//#endregion
export { FilterExpression, FilterExpressionKind, QueryFilterObject, TopLevelFilterExpression, and, code, exactRegex, exclude, exprInterpreter, filterVitePlugins, id, importerId, include, interpreter, interpreterImpl, makeIdFiltersToMatchWithQuery, moduleType, not, or, prefixRegex, queries, query };
@@ -0,0 +1,2 @@
import { _ as queries, a as and, c as exprInterpreter, d as include, f as interpreter, g as or, h as not, i as filterVitePlugins, l as id, m as moduleType, n as makeIdFiltersToMatchWithQuery, o as code, p as interpreterImpl, r as prefixRegex, s as exclude, t as exactRegex, u as importerId, v as query } from "../filter-B_mD-HGz.mjs";
export { and, code, exactRegex, exclude, exprInterpreter, filterVitePlugins, id, importerId, include, interpreter, interpreterImpl, makeIdFiltersToMatchWithQuery, moduleType, not, or, prefixRegex, queries, query };
@@ -0,0 +1,2 @@
import { FilterExpression, FilterExpressionKind, QueryFilterObject, TopLevelFilterExpression, and, code, exactRegex, exclude, exprInterpreter, filterVitePlugins, id, importerId, include, interpreter, interpreterImpl, makeIdFiltersToMatchWithQuery, moduleType, not, or, prefixRegex, queries, query } from "./filter/index.mjs";
export { FilterExpression, FilterExpressionKind, QueryFilterObject, TopLevelFilterExpression, and, code, exactRegex, exclude, exprInterpreter, filterVitePlugins, id, importerId, include, interpreter, interpreterImpl, makeIdFiltersToMatchWithQuery, moduleType, not, or, prefixRegex, queries, query };
@@ -0,0 +1,2 @@
import { _ as queries, a as and, c as exprInterpreter, d as include, f as interpreter, g as or, h as not, i as filterVitePlugins, l as id, m as moduleType, n as makeIdFiltersToMatchWithQuery, o as code, p as interpreterImpl, r as prefixRegex, s as exclude, t as exactRegex, u as importerId, v as query } from "./filter-B_mD-HGz.mjs";
export { and, code, exactRegex, exclude, exprInterpreter, filterVitePlugins, id, importerId, include, interpreter, interpreterImpl, makeIdFiltersToMatchWithQuery, moduleType, not, or, prefixRegex, queries, query };
@@ -0,0 +1,40 @@
{
"name": "@rolldown/pluginutils",
"version": "1.0.1",
"description": "Plugin utilities for Rolldown",
"keywords": [
"filter",
"plugin",
"rolldown"
],
"homepage": "https://github.com/rolldown/plugins/tree/main/packages/pluginutils#readme",
"bugs": {
"url": "https://github.com/rolldown/plugins/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/rolldown/plugins.git",
"directory": "packages/pluginutils"
},
"files": [
"dist"
],
"type": "module",
"exports": {
".": "./dist/index.mjs",
"./filter": "./dist/filter/index.mjs",
"./package.json": "./package.json"
},
"devDependencies": {
"@types/picomatch": "^4.0.3",
"picomatch": "^4.0.4",
"typescript": "^5.9.3"
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"test": "vitest --project pluginutils",
"test:types": "vitest --project pluginutils --typecheck.only"
}
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Toru Nagashima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,98 @@
# abort-controller
[![npm version](https://img.shields.io/npm/v/abort-controller.svg)](https://www.npmjs.com/package/abort-controller)
[![Downloads/month](https://img.shields.io/npm/dm/abort-controller.svg)](http://www.npmtrends.com/abort-controller)
[![Build Status](https://travis-ci.org/mysticatea/abort-controller.svg?branch=master)](https://travis-ci.org/mysticatea/abort-controller)
[![Coverage Status](https://codecov.io/gh/mysticatea/abort-controller/branch/master/graph/badge.svg)](https://codecov.io/gh/mysticatea/abort-controller)
[![Dependency Status](https://david-dm.org/mysticatea/abort-controller.svg)](https://david-dm.org/mysticatea/abort-controller)
An implementation of [WHATWG AbortController interface](https://dom.spec.whatwg.org/#interface-abortcontroller).
```js
import AbortController from "abort-controller"
const controller = new AbortController()
const signal = controller.signal
signal.addEventListener("abort", () => {
console.log("aborted!")
})
controller.abort()
```
> https://jsfiddle.net/1r2994qp/1/
## 💿 Installation
Use [npm](https://www.npmjs.com/) to install then use a bundler.
```
npm install abort-controller
```
Or download from [`dist` directory](./dist).
- [dist/abort-controller.mjs](dist/abort-controller.mjs) ... ES modules version.
- [dist/abort-controller.js](dist/abort-controller.js) ... Common JS version.
- [dist/abort-controller.umd.js](dist/abort-controller.umd.js) ... UMD (Universal Module Definition) version. This is transpiled by [Babel](https://babeljs.io/) for IE 11.
## 📖 Usage
### Basic
```js
import AbortController from "abort-controller"
// or
const AbortController = require("abort-controller")
// or UMD version defines a global variable:
const AbortController = window.AbortControllerShim
```
If your bundler recognizes `browser` field of `package.json`, the imported `AbortController` is the native one and it doesn't contain shim (even if the native implementation was nothing).
If you wanted to polyfill `AbortController` for IE, use `abort-controller/polyfill`.
### Polyfilling
Importing `abort-controller/polyfill` assigns the `AbortController` shim to the `AbortController` global variable if the native implementation was nothing.
```js
import "abort-controller/polyfill"
// or
require("abort-controller/polyfill")
```
### API
#### AbortController
> https://dom.spec.whatwg.org/#interface-abortcontroller
##### controller.signal
The [AbortSignal](https://dom.spec.whatwg.org/#interface-AbortSignal) object which is associated to this controller.
##### controller.abort()
Notify `abort` event to listeners that the `signal` has.
## 📰 Changelog
- See [GitHub releases](https://github.com/mysticatea/abort-controller/releases).
## 🍻 Contributing
Contributing is welcome ❤️
Please use GitHub issues/PRs.
### Development tools
- `npm install` installs dependencies for development.
- `npm test` runs tests and measures code coverage.
- `npm run clean` removes temporary files of tests.
- `npm run coverage` opens code coverage of the previous test with your default browser.
- `npm run lint` runs ESLint.
- `npm run build` generates `dist` codes.
- `npm run watch` runs tests on each file change.
@@ -0,0 +1,13 @@
/*globals self, window */
"use strict"
/*eslint-disable @mysticatea/prettier */
const { AbortController, AbortSignal } =
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window :
/* otherwise */ undefined
/*eslint-enable @mysticatea/prettier */
module.exports = AbortController
module.exports.AbortSignal = AbortSignal
module.exports.default = AbortController
@@ -0,0 +1,11 @@
/*globals self, window */
/*eslint-disable @mysticatea/prettier */
const { AbortController, AbortSignal } =
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window :
/* otherwise */ undefined
/*eslint-enable @mysticatea/prettier */
export default AbortController
export { AbortController, AbortSignal }
@@ -0,0 +1,43 @@
import { EventTarget } from "event-target-shim"
type Events = {
abort: any
}
type EventAttributes = {
onabort: any
}
/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
*/
declare class AbortSignal extends EventTarget<Events, EventAttributes> {
/**
* AbortSignal cannot be constructed directly.
*/
constructor()
/**
* Returns `true` if this `AbortSignal`"s `AbortController` has signaled to abort, and `false` otherwise.
*/
readonly aborted: boolean
}
/**
* The AbortController.
* @see https://dom.spec.whatwg.org/#abortcontroller
*/
declare class AbortController {
/**
* Initialize this controller.
*/
constructor()
/**
* Returns the `AbortSignal` object associated with this object.
*/
readonly signal: AbortSignal
/**
* Abort and signal to any observers that the associated activity is to be aborted.
*/
abort(): void
}
export default AbortController
export { AbortController, AbortSignal }
@@ -0,0 +1,127 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var eventTargetShim = require('event-target-shim');
/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
*/
class AbortSignal extends eventTargetShim.EventTarget {
/**
* AbortSignal cannot be constructed directly.
*/
constructor() {
super();
throw new TypeError("AbortSignal cannot be constructed directly");
}
/**
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
*/
get aborted() {
const aborted = abortedFlags.get(this);
if (typeof aborted !== "boolean") {
throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? "null" : typeof this}`);
}
return aborted;
}
}
eventTargetShim.defineEventAttribute(AbortSignal.prototype, "abort");
/**
* Create an AbortSignal object.
*/
function createAbortSignal() {
const signal = Object.create(AbortSignal.prototype);
eventTargetShim.EventTarget.call(signal);
abortedFlags.set(signal, false);
return signal;
}
/**
* Abort a given signal.
*/
function abortSignal(signal) {
if (abortedFlags.get(signal) !== false) {
return;
}
abortedFlags.set(signal, true);
signal.dispatchEvent({ type: "abort" });
}
/**
* Aborted flag for each instances.
*/
const abortedFlags = new WeakMap();
// Properties should be enumerable.
Object.defineProperties(AbortSignal.prototype, {
aborted: { enumerable: true },
});
// `toString()` should return `"[object AbortSignal]"`
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
configurable: true,
value: "AbortSignal",
});
}
/**
* The AbortController.
* @see https://dom.spec.whatwg.org/#abortcontroller
*/
class AbortController {
/**
* Initialize this controller.
*/
constructor() {
signals.set(this, createAbortSignal());
}
/**
* Returns the `AbortSignal` object associated with this object.
*/
get signal() {
return getSignal(this);
}
/**
* Abort and signal to any observers that the associated activity is to be aborted.
*/
abort() {
abortSignal(getSignal(this));
}
}
/**
* Associated signals.
*/
const signals = new WeakMap();
/**
* Get the associated signal of a given controller.
*/
function getSignal(controller) {
const signal = signals.get(controller);
if (signal == null) {
throw new TypeError(`Expected 'this' to be an 'AbortController' object, but got ${controller === null ? "null" : typeof controller}`);
}
return signal;
}
// Properties should be enumerable.
Object.defineProperties(AbortController.prototype, {
signal: { enumerable: true },
abort: { enumerable: true },
});
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
configurable: true,
value: "AbortController",
});
}
exports.AbortController = AbortController;
exports.AbortSignal = AbortSignal;
exports.default = AbortController;
module.exports = AbortController
module.exports.AbortController = module.exports["default"] = AbortController
module.exports.AbortSignal = AbortSignal
//# sourceMappingURL=abort-controller.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,118 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
* See LICENSE file in root directory for full license.
*/
import { EventTarget, defineEventAttribute } from 'event-target-shim';
/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
*/
class AbortSignal extends EventTarget {
/**
* AbortSignal cannot be constructed directly.
*/
constructor() {
super();
throw new TypeError("AbortSignal cannot be constructed directly");
}
/**
* Returns `true` if this `AbortSignal`'s `AbortController` has signaled to abort, and `false` otherwise.
*/
get aborted() {
const aborted = abortedFlags.get(this);
if (typeof aborted !== "boolean") {
throw new TypeError(`Expected 'this' to be an 'AbortSignal' object, but got ${this === null ? "null" : typeof this}`);
}
return aborted;
}
}
defineEventAttribute(AbortSignal.prototype, "abort");
/**
* Create an AbortSignal object.
*/
function createAbortSignal() {
const signal = Object.create(AbortSignal.prototype);
EventTarget.call(signal);
abortedFlags.set(signal, false);
return signal;
}
/**
* Abort a given signal.
*/
function abortSignal(signal) {
if (abortedFlags.get(signal) !== false) {
return;
}
abortedFlags.set(signal, true);
signal.dispatchEvent({ type: "abort" });
}
/**
* Aborted flag for each instances.
*/
const abortedFlags = new WeakMap();
// Properties should be enumerable.
Object.defineProperties(AbortSignal.prototype, {
aborted: { enumerable: true },
});
// `toString()` should return `"[object AbortSignal]"`
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
configurable: true,
value: "AbortSignal",
});
}
/**
* The AbortController.
* @see https://dom.spec.whatwg.org/#abortcontroller
*/
class AbortController {
/**
* Initialize this controller.
*/
constructor() {
signals.set(this, createAbortSignal());
}
/**
* Returns the `AbortSignal` object associated with this object.
*/
get signal() {
return getSignal(this);
}
/**
* Abort and signal to any observers that the associated activity is to be aborted.
*/
abort() {
abortSignal(getSignal(this));
}
}
/**
* Associated signals.
*/
const signals = new WeakMap();
/**
* Get the associated signal of a given controller.
*/
function getSignal(controller) {
const signal = signals.get(controller);
if (signal == null) {
throw new TypeError(`Expected 'this' to be an 'AbortController' object, but got ${controller === null ? "null" : typeof controller}`);
}
return signal;
}
// Properties should be enumerable.
Object.defineProperties(AbortController.prototype, {
signal: { enumerable: true },
abort: { enumerable: true },
});
if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
Object.defineProperty(AbortController.prototype, Symbol.toStringTag, {
configurable: true,
value: "AbortController",
});
}
export default AbortController;
export { AbortController, AbortSignal };
//# sourceMappingURL=abort-controller.mjs.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,97 @@
{
"name": "abort-controller",
"version": "3.0.0",
"description": "An implementation of WHATWG AbortController interface.",
"main": "dist/abort-controller",
"files": [
"dist",
"polyfill.*",
"browser.*"
],
"engines": {
"node": ">=6.5"
},
"dependencies": {
"event-target-shim": "^5.0.0"
},
"browser": "./browser.js",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
"@babel/preset-env": "^7.3.0",
"@babel/register": "^7.0.0",
"@mysticatea/eslint-plugin": "^8.0.1",
"@mysticatea/spy": "^0.1.2",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.18",
"assert": "^1.4.1",
"codecov": "^3.1.0",
"dts-bundle-generator": "^2.0.0",
"eslint": "^5.12.1",
"karma": "^3.1.4",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.1.0",
"karma-growl-reporter": "^1.0.0",
"karma-ie-launcher": "^1.0.0",
"karma-mocha": "^1.3.0",
"karma-rollup-preprocessor": "^7.0.0-rc.2",
"mocha": "^5.2.0",
"npm-run-all": "^4.1.5",
"nyc": "^13.1.0",
"opener": "^1.5.1",
"rimraf": "^2.6.3",
"rollup": "^1.1.2",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-babel-minify": "^7.0.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript": "^1.0.0",
"rollup-watch": "^4.3.1",
"ts-node": "^8.0.1",
"type-tester": "^1.0.0",
"typescript": "^3.2.4"
},
"scripts": {
"preversion": "npm test",
"version": "npm run -s build && git add dist/*",
"postversion": "git push && git push --tags",
"clean": "rimraf .nyc_output coverage",
"coverage": "opener coverage/lcov-report/index.html",
"lint": "eslint . --ext .ts",
"build": "run-s -s build:*",
"build:rollup": "rollup -c",
"build:dts": "dts-bundle-generator -o dist/abort-controller.d.ts src/abort-controller.ts && ts-node scripts/fix-dts",
"test": "run-s -s lint test:*",
"test:mocha": "nyc mocha test/*.ts",
"test:karma": "karma start --single-run",
"watch": "run-p -s watch:*",
"watch:mocha": "mocha test/*.ts --require ts-node/register --watch-extensions ts --watch --growl",
"watch:karma": "karma start --watch",
"codecov": "codecov"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mysticatea/abort-controller.git"
},
"keywords": [
"w3c",
"whatwg",
"event",
"events",
"abort",
"cancel",
"abortcontroller",
"abortsignal",
"controller",
"signal",
"shim"
],
"author": "Toru Nagashima (https://github.com/mysticatea)",
"license": "MIT",
"bugs": {
"url": "https://github.com/mysticatea/abort-controller/issues"
},
"homepage": "https://github.com/mysticatea/abort-controller#readme"
}
@@ -0,0 +1,21 @@
/*globals require, self, window */
"use strict"
const ac = require("./dist/abort-controller")
/*eslint-disable @mysticatea/prettier */
const g =
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window :
typeof global !== "undefined" ? global :
/* otherwise */ undefined
/*eslint-enable @mysticatea/prettier */
if (g) {
if (typeof g.AbortController === "undefined") {
g.AbortController = ac.AbortController
}
if (typeof g.AbortSignal === "undefined") {
g.AbortSignal = ac.AbortSignal
}
}
@@ -0,0 +1,19 @@
/*globals self, window */
import * as ac from "./dist/abort-controller"
/*eslint-disable @mysticatea/prettier */
const g =
typeof self !== "undefined" ? self :
typeof window !== "undefined" ? window :
typeof global !== "undefined" ? global :
/* otherwise */ undefined
/*eslint-enable @mysticatea/prettier */
if (g) {
if (typeof g.AbortController === "undefined") {
g.AbortController = ac.AbortController
}
if (typeof g.AbortSignal === "undefined") {
g.AbortSignal = ac.AbortSignal
}
}
+145
View File
@@ -0,0 +1,145 @@
agent-base
==========
### Turn a function into an [`http.Agent`][http.Agent] instance
[![Build Status](https://github.com/TooTallNate/node-agent-base/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI)
This module provides an `http.Agent` generator. That is, you pass it an async
callback function, and it returns a new `http.Agent` instance that will invoke the
given callback function when sending outbound HTTP requests.
#### Some subclasses:
Here's some more interesting uses of `agent-base`.
Send a pull request to list yours!
* [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints
* [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints
* [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS
* [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
Installation
------------
Install with `npm`:
``` bash
$ npm install agent-base
```
Example
-------
Here's a minimal example that creates a new `net.Socket` connection to the server
for every HTTP request (i.e. the equivalent of `agent: false` option):
```js
var net = require('net');
var tls = require('tls');
var url = require('url');
var http = require('http');
var agent = require('agent-base');
var endpoint = 'http://nodejs.org/api/';
var parsed = url.parse(endpoint);
// This is the important part!
parsed.agent = agent(function (req, opts) {
var socket;
// `secureEndpoint` is true when using the https module
if (opts.secureEndpoint) {
socket = tls.connect(opts);
} else {
socket = net.connect(opts);
}
return socket;
});
// Everything else works just like normal...
http.get(parsed, function (res) {
console.log('"response" event!', res.headers);
res.pipe(process.stdout);
});
```
Returning a Promise or using an `async` function is also supported:
```js
agent(async function (req, opts) {
await sleep(1000);
// etc…
});
```
Return another `http.Agent` instance to "pass through" the responsibility
for that HTTP request to that agent:
```js
agent(function (req, opts) {
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
});
```
API
---
## Agent(Function callback[, Object options]) → [http.Agent][]
Creates a base `http.Agent` that will execute the callback function `callback`
for every HTTP request that it is used as the `agent` for. The callback function
is responsible for creating a `stream.Duplex` instance of some kind that will be
used as the underlying socket in the HTTP request.
The `options` object accepts the following properties:
* `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional).
The callback function should have the following signature:
### callback(http.ClientRequest req, Object options, Function cb) → undefined
The ClientRequest `req` can be accessed to read request headers and
and the path, etc. The `options` object contains the options passed
to the `http.request()`/`https.request()` function call, and is formatted
to be directly passed to `net.connect()`/`tls.connect()`, or however
else you want a Socket to be created. Pass the created socket to
the callback function `cb` once created, and the HTTP request will
continue to proceed.
If the `https` module is used to invoke the HTTP request, then the
`secureEndpoint` property on `options` _will be set to `true`_.
License
-------
(The MIT License)
Copyright (c) 2013 Nathan Rajlich &lt;nathan@tootallnate.net&gt;
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent
@@ -0,0 +1,78 @@
/// <reference types="node" />
import net from 'net';
import http from 'http';
import https from 'https';
import { Duplex } from 'stream';
import { EventEmitter } from 'events';
declare function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
declare function createAgent(callback: createAgent.AgentCallback, opts?: createAgent.AgentOptions): createAgent.Agent;
declare namespace createAgent {
interface ClientRequest extends http.ClientRequest {
_last?: boolean;
_hadError?: boolean;
method: string;
}
interface AgentRequestOptions {
host?: string;
path?: string;
port: number;
}
interface HttpRequestOptions extends AgentRequestOptions, Omit<http.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: false;
}
interface HttpsRequestOptions extends AgentRequestOptions, Omit<https.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: true;
}
type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
type AgentCallbackReturn = Duplex | AgentLike;
type AgentCallbackCallback = (err?: Error | null, socket?: createAgent.AgentCallbackReturn) => void;
type AgentCallbackPromise = (req: createAgent.ClientRequest, opts: createAgent.RequestOptions) => createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
type AgentCallback = typeof Agent.prototype.callback;
type AgentOptions = {
timeout?: number;
};
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
class Agent extends EventEmitter {
timeout: number | null;
maxFreeSockets: number;
maxTotalSockets: number;
maxSockets: number;
sockets: {
[key: string]: net.Socket[];
};
freeSockets: {
[key: string]: net.Socket[];
};
requests: {
[key: string]: http.IncomingMessage[];
};
options: https.AgentOptions;
private promisifiedCallback?;
private explicitDefaultPort?;
private explicitProtocol?;
constructor(callback?: createAgent.AgentCallback | createAgent.AgentOptions, _opts?: createAgent.AgentOptions);
get defaultPort(): number;
set defaultPort(v: number);
get protocol(): string;
set protocol(v: string);
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions, fn: createAgent.AgentCallbackCallback): void;
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions): createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req: ClientRequest, _opts: RequestOptions): void;
freeSocket(socket: net.Socket, opts: AgentOptions): void;
destroy(): void;
}
}
export = createAgent;
@@ -0,0 +1,203 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const promisify_1 = __importDefault(require("./promisify"));
const debug = debug_1.default('agent-base');
function isAgent(v) {
return Boolean(v) && typeof v.addRequest === 'function';
}
function isSecureEndpoint() {
const { stack } = new Error();
if (typeof stack !== 'string')
return false;
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
}
function createAgent(callback, opts) {
return new createAgent.Agent(callback, opts);
}
(function (createAgent) {
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
class Agent extends events_1.EventEmitter {
constructor(callback, _opts) {
super();
let opts = _opts;
if (typeof callback === 'function') {
this.callback = callback;
}
else if (callback) {
opts = callback;
}
// Timeout for the socket to be returned from the callback
this.timeout = null;
if (opts && typeof opts.timeout === 'number') {
this.timeout = opts.timeout;
}
// These aren't actually used by `agent-base`, but are required
// for the TypeScript definition files in `@types/node` :/
this.maxFreeSockets = 1;
this.maxSockets = 1;
this.maxTotalSockets = Infinity;
this.sockets = {};
this.freeSockets = {};
this.requests = {};
this.options = {};
}
get defaultPort() {
if (typeof this.explicitDefaultPort === 'number') {
return this.explicitDefaultPort;
}
return isSecureEndpoint() ? 443 : 80;
}
set defaultPort(v) {
this.explicitDefaultPort = v;
}
get protocol() {
if (typeof this.explicitProtocol === 'string') {
return this.explicitProtocol;
}
return isSecureEndpoint() ? 'https:' : 'http:';
}
set protocol(v) {
this.explicitProtocol = v;
}
callback(req, opts, fn) {
throw new Error('"agent-base" has no default implementation, you must subclass and override `callback()`');
}
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req, _opts) {
const opts = Object.assign({}, _opts);
if (typeof opts.secureEndpoint !== 'boolean') {
opts.secureEndpoint = isSecureEndpoint();
}
if (opts.host == null) {
opts.host = 'localhost';
}
if (opts.port == null) {
opts.port = opts.secureEndpoint ? 443 : 80;
}
if (opts.protocol == null) {
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
}
if (opts.host && opts.path) {
// If both a `host` and `path` are specified then it's most
// likely the result of a `url.parse()` call... we need to
// remove the `path` portion so that `net.connect()` doesn't
// attempt to open that as a unix socket file.
delete opts.path;
}
delete opts.agent;
delete opts.hostname;
delete opts._defaultAgent;
delete opts.defaultPort;
delete opts.createConnection;
// Hint to use "Connection: close"
// XXX: non-documented `http` module API :(
req._last = true;
req.shouldKeepAlive = false;
let timedOut = false;
let timeoutId = null;
const timeoutMs = opts.timeout || this.timeout;
const onerror = (err) => {
if (req._hadError)
return;
req.emit('error', err);
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req._hadError = true;
};
const ontimeout = () => {
timeoutId = null;
timedOut = true;
const err = new Error(`A "socket" was not created for HTTP request before ${timeoutMs}ms`);
err.code = 'ETIMEOUT';
onerror(err);
};
const callbackError = (err) => {
if (timedOut)
return;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
onerror(err);
};
const onsocket = (socket) => {
if (timedOut)
return;
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (isAgent(socket)) {
// `socket` is actually an `http.Agent` instance, so
// relinquish responsibility for this `req` to the Agent
// from here on
debug('Callback returned another Agent instance %o', socket.constructor.name);
socket.addRequest(req, opts);
return;
}
if (socket) {
socket.once('free', () => {
this.freeSocket(socket, opts);
});
req.onSocket(socket);
return;
}
const err = new Error(`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``);
onerror(err);
};
if (typeof this.callback !== 'function') {
onerror(new Error('`callback` is not defined'));
return;
}
if (!this.promisifiedCallback) {
if (this.callback.length >= 3) {
debug('Converting legacy callback function to promise');
this.promisifiedCallback = promisify_1.default(this.callback);
}
else {
this.promisifiedCallback = this.callback;
}
}
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
timeoutId = setTimeout(ontimeout, timeoutMs);
}
if ('port' in opts && typeof opts.port !== 'number') {
opts.port = Number(opts.port);
}
try {
debug('Resolving socket for %o request: %o', opts.protocol, `${req.method} ${req.path}`);
Promise.resolve(this.promisifiedCallback(req, opts)).then(onsocket, callbackError);
}
catch (err) {
Promise.reject(err).catch(callbackError);
}
}
freeSocket(socket, opts) {
debug('Freeing socket %o %o', socket.constructor.name, opts);
socket.destroy();
}
destroy() {
debug('Destroying agent %o', this.constructor.name);
}
}
createAgent.Agent = Agent;
// So that `instanceof` works correctly
createAgent.prototype = createAgent.Agent.prototype;
})(createAgent || (createAgent = {}));
module.exports = createAgent;
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
import { ClientRequest, RequestOptions, AgentCallbackCallback, AgentCallbackPromise } from './index';
declare type LegacyCallback = (req: ClientRequest, opts: RequestOptions, fn: AgentCallbackCallback) => void;
export default function promisify(fn: LegacyCallback): AgentCallbackPromise;
export {};
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function promisify(fn) {
return function (req, opts) {
return new Promise((resolve, reject) => {
fn.call(this, req, opts, (err, rtn) => {
if (err) {
reject(err);
}
else {
resolve(rtn);
}
});
});
};
}
exports.default = promisify;
//# sourceMappingURL=promisify.js.map
@@ -0,0 +1 @@
{"version":3,"file":"promisify.js","sourceRoot":"","sources":["../../src/promisify.ts"],"names":[],"mappings":";;AAeA,SAAwB,SAAS,CAAC,EAAkB;IACnD,OAAO,UAAsB,GAAkB,EAAE,IAAoB;QACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtC,EAAE,CAAC,IAAI,CACN,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,CAAC,GAA6B,EAAE,GAAyB,EAAE,EAAE;gBAC5D,IAAI,GAAG,EAAE;oBACR,MAAM,CAAC,GAAG,CAAC,CAAC;iBACZ;qBAAM;oBACN,OAAO,CAAC,GAAG,CAAC,CAAC;iBACb;YACF,CAAC,CACD,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;AACH,CAAC;AAjBD,4BAiBC"}
@@ -0,0 +1,64 @@
{
"name": "agent-base",
"version": "6.0.2",
"description": "Turn a function into an `http.Agent` instance",
"main": "dist/src/index",
"typings": "dist/src/index",
"files": [
"dist/src",
"src"
],
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc",
"postbuild": "cpy --parents src test '!**/*.ts' dist",
"test": "mocha --reporter spec dist/test/*.js",
"test-lint": "eslint src --ext .js,.ts",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git://github.com/TooTallNate/node-agent-base.git"
},
"keywords": [
"http",
"agent",
"base",
"barebones",
"https"
],
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/TooTallNate/node-agent-base/issues"
},
"dependencies": {
"debug": "4"
},
"devDependencies": {
"@types/debug": "4",
"@types/mocha": "^5.2.7",
"@types/node": "^14.0.20",
"@types/semver": "^7.1.0",
"@types/ws": "^6.0.3",
"@typescript-eslint/eslint-plugin": "1.6.0",
"@typescript-eslint/parser": "1.1.0",
"async-listen": "^1.2.0",
"cpy-cli": "^2.0.0",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-prettier": "4.1.0",
"eslint-import-resolver-typescript": "1.1.1",
"eslint-plugin-import": "2.16.0",
"eslint-plugin-jsx-a11y": "6.2.1",
"eslint-plugin-react": "7.12.4",
"mocha": "^6.2.0",
"rimraf": "^3.0.0",
"semver": "^7.1.2",
"typescript": "^3.5.3",
"ws": "^3.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
}
@@ -0,0 +1,345 @@
import net from 'net';
import http from 'http';
import https from 'https';
import { Duplex } from 'stream';
import { EventEmitter } from 'events';
import createDebug from 'debug';
import promisify from './promisify';
const debug = createDebug('agent-base');
function isAgent(v: any): v is createAgent.AgentLike {
return Boolean(v) && typeof v.addRequest === 'function';
}
function isSecureEndpoint(): boolean {
const { stack } = new Error();
if (typeof stack !== 'string') return false;
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
}
function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
function createAgent(
callback: createAgent.AgentCallback,
opts?: createAgent.AgentOptions
): createAgent.Agent;
function createAgent(
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
opts?: createAgent.AgentOptions
) {
return new createAgent.Agent(callback, opts);
}
namespace createAgent {
export interface ClientRequest extends http.ClientRequest {
_last?: boolean;
_hadError?: boolean;
method: string;
}
export interface AgentRequestOptions {
host?: string;
path?: string;
// `port` on `http.RequestOptions` can be a string or undefined,
// but `net.TcpNetConnectOpts` expects only a number
port: number;
}
export interface HttpRequestOptions
extends AgentRequestOptions,
Omit<http.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: false;
}
export interface HttpsRequestOptions
extends AgentRequestOptions,
Omit<https.RequestOptions, keyof AgentRequestOptions> {
secureEndpoint: true;
}
export type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
export type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
export type AgentCallbackReturn = Duplex | AgentLike;
export type AgentCallbackCallback = (
err?: Error | null,
socket?: createAgent.AgentCallbackReturn
) => void;
export type AgentCallbackPromise = (
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions
) =>
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>;
export type AgentCallback = typeof Agent.prototype.callback;
export type AgentOptions = {
timeout?: number;
};
/**
* Base `http.Agent` implementation.
* No pooling/keep-alive is implemented by default.
*
* @param {Function} callback
* @api public
*/
export class Agent extends EventEmitter {
public timeout: number | null;
public maxFreeSockets: number;
public maxTotalSockets: number;
public maxSockets: number;
public sockets: {
[key: string]: net.Socket[];
};
public freeSockets: {
[key: string]: net.Socket[];
};
public requests: {
[key: string]: http.IncomingMessage[];
};
public options: https.AgentOptions;
private promisifiedCallback?: createAgent.AgentCallbackPromise;
private explicitDefaultPort?: number;
private explicitProtocol?: string;
constructor(
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
_opts?: createAgent.AgentOptions
) {
super();
let opts = _opts;
if (typeof callback === 'function') {
this.callback = callback;
} else if (callback) {
opts = callback;
}
// Timeout for the socket to be returned from the callback
this.timeout = null;
if (opts && typeof opts.timeout === 'number') {
this.timeout = opts.timeout;
}
// These aren't actually used by `agent-base`, but are required
// for the TypeScript definition files in `@types/node` :/
this.maxFreeSockets = 1;
this.maxSockets = 1;
this.maxTotalSockets = Infinity;
this.sockets = {};
this.freeSockets = {};
this.requests = {};
this.options = {};
}
get defaultPort(): number {
if (typeof this.explicitDefaultPort === 'number') {
return this.explicitDefaultPort;
}
return isSecureEndpoint() ? 443 : 80;
}
set defaultPort(v: number) {
this.explicitDefaultPort = v;
}
get protocol(): string {
if (typeof this.explicitProtocol === 'string') {
return this.explicitProtocol;
}
return isSecureEndpoint() ? 'https:' : 'http:';
}
set protocol(v: string) {
this.explicitProtocol = v;
}
callback(
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions,
fn: createAgent.AgentCallbackCallback
): void;
callback(
req: createAgent.ClientRequest,
opts: createAgent.RequestOptions
):
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>;
callback(
req: createAgent.ClientRequest,
opts: createAgent.AgentOptions,
fn?: createAgent.AgentCallbackCallback
):
| createAgent.AgentCallbackReturn
| Promise<createAgent.AgentCallbackReturn>
| void {
throw new Error(
'"agent-base" has no default implementation, you must subclass and override `callback()`'
);
}
/**
* Called by node-core's "_http_client.js" module when creating
* a new HTTP request with this Agent instance.
*
* @api public
*/
addRequest(req: ClientRequest, _opts: RequestOptions): void {
const opts: RequestOptions = { ..._opts };
if (typeof opts.secureEndpoint !== 'boolean') {
opts.secureEndpoint = isSecureEndpoint();
}
if (opts.host == null) {
opts.host = 'localhost';
}
if (opts.port == null) {
opts.port = opts.secureEndpoint ? 443 : 80;
}
if (opts.protocol == null) {
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
}
if (opts.host && opts.path) {
// If both a `host` and `path` are specified then it's most
// likely the result of a `url.parse()` call... we need to
// remove the `path` portion so that `net.connect()` doesn't
// attempt to open that as a unix socket file.
delete opts.path;
}
delete opts.agent;
delete opts.hostname;
delete opts._defaultAgent;
delete opts.defaultPort;
delete opts.createConnection;
// Hint to use "Connection: close"
// XXX: non-documented `http` module API :(
req._last = true;
req.shouldKeepAlive = false;
let timedOut = false;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const timeoutMs = opts.timeout || this.timeout;
const onerror = (err: NodeJS.ErrnoException) => {
if (req._hadError) return;
req.emit('error', err);
// For Safety. Some additional errors might fire later on
// and we need to make sure we don't double-fire the error event.
req._hadError = true;
};
const ontimeout = () => {
timeoutId = null;
timedOut = true;
const err: NodeJS.ErrnoException = new Error(
`A "socket" was not created for HTTP request before ${timeoutMs}ms`
);
err.code = 'ETIMEOUT';
onerror(err);
};
const callbackError = (err: NodeJS.ErrnoException) => {
if (timedOut) return;
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
onerror(err);
};
const onsocket = (socket: AgentCallbackReturn) => {
if (timedOut) return;
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (isAgent(socket)) {
// `socket` is actually an `http.Agent` instance, so
// relinquish responsibility for this `req` to the Agent
// from here on
debug(
'Callback returned another Agent instance %o',
socket.constructor.name
);
(socket as createAgent.Agent).addRequest(req, opts);
return;
}
if (socket) {
socket.once('free', () => {
this.freeSocket(socket as net.Socket, opts);
});
req.onSocket(socket as net.Socket);
return;
}
const err = new Error(
`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``
);
onerror(err);
};
if (typeof this.callback !== 'function') {
onerror(new Error('`callback` is not defined'));
return;
}
if (!this.promisifiedCallback) {
if (this.callback.length >= 3) {
debug('Converting legacy callback function to promise');
this.promisifiedCallback = promisify(this.callback);
} else {
this.promisifiedCallback = this.callback;
}
}
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
timeoutId = setTimeout(ontimeout, timeoutMs);
}
if ('port' in opts && typeof opts.port !== 'number') {
opts.port = Number(opts.port);
}
try {
debug(
'Resolving socket for %o request: %o',
opts.protocol,
`${req.method} ${req.path}`
);
Promise.resolve(this.promisifiedCallback(req, opts)).then(
onsocket,
callbackError
);
} catch (err) {
Promise.reject(err).catch(callbackError);
}
}
freeSocket(socket: net.Socket, opts: AgentOptions) {
debug('Freeing socket %o %o', socket.constructor.name, opts);
socket.destroy();
}
destroy() {
debug('Destroying agent %o', this.constructor.name);
}
}
// So that `instanceof` works correctly
createAgent.prototype = createAgent.Agent.prototype;
}
export = createAgent;
@@ -0,0 +1,33 @@
import {
Agent,
ClientRequest,
RequestOptions,
AgentCallbackCallback,
AgentCallbackPromise,
AgentCallbackReturn
} from './index';
type LegacyCallback = (
req: ClientRequest,
opts: RequestOptions,
fn: AgentCallbackCallback
) => void;
export default function promisify(fn: LegacyCallback): AgentCallbackPromise {
return function(this: Agent, req: ClientRequest, opts: RequestOptions) {
return new Promise((resolve, reject) => {
fn.call(
this,
req,
opts,
(err: Error | null | undefined, rtn?: AgentCallbackReturn) => {
if (err) {
reject(err);
} else {
resolve(rtn);
}
}
);
});
};
}
+37
View File
@@ -0,0 +1,37 @@
declare namespace ansiRegex {
interface Options {
/**
Match only the first ANSI escape.
@default false
*/
onlyFirst: boolean;
}
}
/**
Regular expression for matching ANSI escape codes.
@example
```
import ansiRegex = require('ansi-regex');
ansiRegex().test('\u001B[4mcake\u001B[0m');
//=> true
ansiRegex().test('cake');
//=> false
'\u001B[4mcake\u001B[0m'.match(ansiRegex());
//=> ['\u001B[4m', '\u001B[0m']
'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true}));
//=> ['\u001B[4m']
'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex());
//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007']
```
*/
declare function ansiRegex(options?: ansiRegex.Options): RegExp;
export = ansiRegex;

Some files were not shown because too many files have changed in this diff Show More