Merge master branch with actual project content
Deploy EternalAI / deploy (push) Failing after 3h0m0s
Details
Deploy EternalAI / deploy (push) Failing after 3h0m0s
Details
This commit is contained in:
commit
a272c62b97
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Deploy EternalAI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: ./scripts/deploy.sh
|
||||||
Binary file not shown.
14
README.md
14
README.md
|
|
@ -1,3 +1,15 @@
|
||||||
# EternalAI
|
# EternalAI
|
||||||
|
|
||||||
EternalAI Project Repository// Update to main branch
|
一个永恒的 AI 系统,旨在长期运行和自我进化。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
- `src/` - 源代码
|
||||||
|
- `docs/` - 文档
|
||||||
|
- `tests/` - 测试文件
|
||||||
|
- `scripts/` - 脚本
|
||||||
|
- `configs/` - 配置文件
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
该项目将通过 Gitea Actions 自动部署。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,907 @@
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// --- U9: Unified state management ---
|
||||||
|
const STORAGE_KEY = 'eternal_ai_state';
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
isLoggedIn: false,
|
||||||
|
isCreator: false,
|
||||||
|
account: null,
|
||||||
|
boundCreator: null,
|
||||||
|
libraryName: '我的 [XXX]',
|
||||||
|
creatorName: '',
|
||||||
|
roles: [],
|
||||||
|
income: { balance: 0, records: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadState() {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||||
|
return { ...defaultState, ...saved };
|
||||||
|
} catch {
|
||||||
|
return { ...defaultState };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState() {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = loadState();
|
||||||
|
|
||||||
|
// --- Mock data for role library (U2) ---
|
||||||
|
const mockRoles = [
|
||||||
|
{
|
||||||
|
id: 'role_001',
|
||||||
|
name: '云朵',
|
||||||
|
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20soft%20pastel%20portrait%20gentle%20smile&image_size=square',
|
||||||
|
desc: '温柔如云的女孩,总是轻声细语地陪伴你。',
|
||||||
|
price: 29.9,
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role_002',
|
||||||
|
name: '星河',
|
||||||
|
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20boy%20starry%20eyes%20cool%20portrait&image_size=square',
|
||||||
|
desc: '嘴硬心软的少年,嘴上不饶人却总在关键时刻出现。',
|
||||||
|
price: 39.9,
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role_003',
|
||||||
|
name: '月见',
|
||||||
|
avatar: 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=anime%20girl%20moonlight%20mysterious%20elegant%20portrait&image_size=square',
|
||||||
|
desc: '神秘而优雅,像月光一样忽远忽近的存在。',
|
||||||
|
price: 19.9,
|
||||||
|
status: 'stopped',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockIncome = {
|
||||||
|
balance: 1280.50,
|
||||||
|
records: [
|
||||||
|
{ time: '2026-06-18 14:30', amount: 23.92, role: '云朵' },
|
||||||
|
{ time: '2026-06-15 09:12', amount: 31.92, role: '星河' },
|
||||||
|
{ time: '2026-06-10 20:05', amount: 15.92, role: '月见' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- DOM references ---
|
||||||
|
const views = {
|
||||||
|
landing: document.getElementById('landing'),
|
||||||
|
auth: document.getElementById('auth'),
|
||||||
|
'role-library': document.getElementById('role-library'),
|
||||||
|
'role-detail': document.getElementById('role-detail'),
|
||||||
|
distill: document.getElementById('distill'),
|
||||||
|
about: document.getElementById('about'),
|
||||||
|
onboarding: document.getElementById('onboarding'),
|
||||||
|
'creator-center': document.getElementById('creator-center'),
|
||||||
|
creator: document.getElementById('creator'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = document.getElementById('character-form');
|
||||||
|
const resultPanel = document.getElementById('result-panel');
|
||||||
|
const previewCode = document.querySelector('#preview-code code');
|
||||||
|
const systemPromptInput = document.getElementById('system-prompt');
|
||||||
|
const steps = Array.from(document.querySelectorAll('.form-step'));
|
||||||
|
const dots = Array.from(document.querySelectorAll('#creator .stepper__dot'));
|
||||||
|
let currentStep = 0;
|
||||||
|
let generatedSoul = '';
|
||||||
|
let generatedConfig = '';
|
||||||
|
let activePreview = 'soul';
|
||||||
|
let activeAuthTab = 'login';
|
||||||
|
let activeCenterTab = 'roles';
|
||||||
|
let currentRole = null;
|
||||||
|
let viewHistory = ['landing'];
|
||||||
|
|
||||||
|
// --- U9: Unified showView with history and tab-bar sync ---
|
||||||
|
// Human-readable labels for screen-reader announcements
|
||||||
|
const viewLabels = {
|
||||||
|
landing: '首页',
|
||||||
|
auth: '登录 / 注册',
|
||||||
|
'role-library': '角色库',
|
||||||
|
'role-detail': '角色详情',
|
||||||
|
distill: '蒸馏前任',
|
||||||
|
about: '关于 Eternal AI',
|
||||||
|
onboarding: '创作者入驻',
|
||||||
|
'creator-center': '创作者管理中心',
|
||||||
|
creator: '角色编辑',
|
||||||
|
};
|
||||||
|
|
||||||
|
function showView(name, trackHistory = true) {
|
||||||
|
Object.entries(views).forEach(([key, el]) => {
|
||||||
|
if (el) el.classList.toggle('active', key === name);
|
||||||
|
});
|
||||||
|
if (trackHistory && viewHistory[viewHistory.length - 1] !== name) {
|
||||||
|
viewHistory.push(name);
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
updateTabBar(name);
|
||||||
|
|
||||||
|
// a11y: move focus to the new view so screen readers announce it
|
||||||
|
const target = views[name];
|
||||||
|
if (target) {
|
||||||
|
target.setAttribute('tabindex', '-1');
|
||||||
|
// Defer focus to after the scroll/layout settles
|
||||||
|
setTimeout(() => target.focus({ preventScroll: true }), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: announce the view change to screen readers via live region
|
||||||
|
const announcer = document.getElementById('sr-announce');
|
||||||
|
if (announcer) {
|
||||||
|
announcer.textContent = viewLabels[name] ? `已进入${viewLabels[name]}` : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (viewHistory.length > 1) {
|
||||||
|
viewHistory.pop();
|
||||||
|
const prev = viewHistory[viewHistory.length - 1];
|
||||||
|
showView(prev, false);
|
||||||
|
} else {
|
||||||
|
showView('landing', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U8: Tab bar ---
|
||||||
|
function updateTabBar(viewName) {
|
||||||
|
const tabMap = {
|
||||||
|
landing: 'tab-home',
|
||||||
|
distill: 'tab-distill',
|
||||||
|
'role-library': 'tab-mine',
|
||||||
|
'creator-center': 'tab-mine',
|
||||||
|
};
|
||||||
|
const activeTab = tabMap[viewName] || 'tab-home';
|
||||||
|
document.querySelectorAll('.tab-bar__item').forEach((item) => {
|
||||||
|
const isActive = item.dataset.tabAction === activeTab;
|
||||||
|
item.classList.toggle('active', isActive);
|
||||||
|
item.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabAction(action) {
|
||||||
|
if (action === 'tab-home') {
|
||||||
|
showView('landing');
|
||||||
|
} else if (action === 'tab-distill') {
|
||||||
|
showView('distill');
|
||||||
|
} else if (action === 'tab-mine') {
|
||||||
|
if (!state.isLoggedIn) {
|
||||||
|
switchAuthTab('login');
|
||||||
|
showView('auth');
|
||||||
|
} else if (state.isCreator) {
|
||||||
|
showView('creator-center');
|
||||||
|
renderCreatorCenter();
|
||||||
|
} else {
|
||||||
|
renderRoleLibrary();
|
||||||
|
showView('role-library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U1: Landing card state ---
|
||||||
|
function updateLandingCard() {
|
||||||
|
const nameEl = document.getElementById('library-name');
|
||||||
|
const descEl = document.getElementById('characters-desc');
|
||||||
|
const btnEl = document.getElementById('characters-btn');
|
||||||
|
const tabMineLabel = document.getElementById('tab-mine-label');
|
||||||
|
|
||||||
|
if (state.isLoggedIn) {
|
||||||
|
nameEl.textContent = state.libraryName || '我的角色库';
|
||||||
|
if (state.isCreator) {
|
||||||
|
descEl.textContent = '管理你的角色和收入';
|
||||||
|
btnEl.textContent = '进入管理中心';
|
||||||
|
tabMineLabel.textContent = '管理';
|
||||||
|
} else {
|
||||||
|
descEl.textContent = state.boundCreator ? '查看你的专属角色' : '寻找你的专属创作者';
|
||||||
|
btnEl.textContent = '进入角色库';
|
||||||
|
tabMineLabel.textContent = '我的';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nameEl.textContent = '我的 [XXX]';
|
||||||
|
descEl.textContent = '登录后管理你的角色';
|
||||||
|
btnEl.textContent = '登录 / 注册';
|
||||||
|
tabMineLabel.textContent = '我的';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auth ---
|
||||||
|
function switchAuthTab(tab) {
|
||||||
|
activeAuthTab = tab;
|
||||||
|
document.querySelectorAll('.auth-tab').forEach((t) => {
|
||||||
|
const isActive = t.dataset.tab === tab;
|
||||||
|
t.classList.toggle('active', isActive);
|
||||||
|
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.auth-form').forEach((f) => {
|
||||||
|
f.classList.toggle('active', f.dataset.form === tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasswordMatch(formEl) {
|
||||||
|
const pwd = formEl.querySelector('[name="password"]');
|
||||||
|
const confirm = formEl.querySelector('[name="confirmPassword"]');
|
||||||
|
if (!pwd || !confirm) return true;
|
||||||
|
if (pwd.value !== confirm.value) {
|
||||||
|
confirm.setCustomValidity('两次输入的密码不一致');
|
||||||
|
confirm.reportValidity();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
confirm.setCustomValidity('');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(account) {
|
||||||
|
state.isLoggedIn = true;
|
||||||
|
state.account = account;
|
||||||
|
state.boundCreator = state.boundCreator || { name: '云朵', roles: mockRoles };
|
||||||
|
saveState();
|
||||||
|
updateLandingCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
state.isLoggedIn = false;
|
||||||
|
state.isCreator = false;
|
||||||
|
state.account = null;
|
||||||
|
saveState();
|
||||||
|
updateLandingCard();
|
||||||
|
showView('landing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U2: Role Library ---
|
||||||
|
function renderRoleLibrary() {
|
||||||
|
const listEl = document.getElementById('role-list');
|
||||||
|
const emptyEl = document.getElementById('library-empty');
|
||||||
|
const titleEl = document.getElementById('library-title');
|
||||||
|
|
||||||
|
titleEl.textContent = state.libraryName || '我的角色库';
|
||||||
|
|
||||||
|
if (!state.boundCreator) {
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
emptyEl.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyEl.hidden = true;
|
||||||
|
listEl.innerHTML = mockRoles
|
||||||
|
.map(
|
||||||
|
(role) => `
|
||||||
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.desc},每月${role.price}元">
|
||||||
|
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||||
|
<div class="role-card__info">
|
||||||
|
<h3 class="role-card__name">${role.name}</h3>
|
||||||
|
<p class="role-card__desc">${role.desc}</p>
|
||||||
|
<span class="role-card__price">¥${role.price}</span>
|
||||||
|
</div>
|
||||||
|
</article>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U3: Role Detail ---
|
||||||
|
function renderRoleDetail(roleId) {
|
||||||
|
const role = mockRoles.find((r) => r.id === roleId);
|
||||||
|
if (!role) return;
|
||||||
|
currentRole = role;
|
||||||
|
|
||||||
|
document.getElementById('detail-name').textContent = role.name;
|
||||||
|
document.getElementById('detail-hero').style.backgroundImage = `url(${role.avatar})`;
|
||||||
|
document.getElementById('detail-role-name').textContent = role.name;
|
||||||
|
document.getElementById('detail-role-desc').textContent = role.desc;
|
||||||
|
document.getElementById('detail-price').innerHTML = `<span class="detail-price__value">¥${role.price}</span><span class="detail-price__unit">/ 月</span>`;
|
||||||
|
|
||||||
|
document.getElementById('detail-actions-pre').hidden = false;
|
||||||
|
document.getElementById('detail-paid').hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function payRole() {
|
||||||
|
document.getElementById('detail-actions-pre').hidden = true;
|
||||||
|
const paidEl = document.getElementById('detail-paid');
|
||||||
|
paidEl.hidden = false;
|
||||||
|
document.getElementById('detail-qr').innerHTML = '<div class="qr-placeholder">扫码连接<br/>AI 角色</div>';
|
||||||
|
document.getElementById('detail-avatar').style.backgroundImage = `url(${currentRole.avatar})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U4: About FAQ ---
|
||||||
|
function toggleFaq(button) {
|
||||||
|
const item = button.closest('.faq-item');
|
||||||
|
const answer = item.querySelector('.faq-a');
|
||||||
|
const icon = button.querySelector('.faq-icon');
|
||||||
|
const isOpen = answer.style.display === 'block';
|
||||||
|
answer.style.display = isOpen ? 'none' : 'block';
|
||||||
|
icon.textContent = isOpen ? '+' : '−';
|
||||||
|
// a11y: sync aria-expanded so screen readers know the toggle state
|
||||||
|
button.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: wire up FAQ buttons with aria-expanded / aria-controls on load
|
||||||
|
function initFaqA11y() {
|
||||||
|
const faqButtons = document.querySelectorAll('.faq-q');
|
||||||
|
faqButtons.forEach((btn, index) => {
|
||||||
|
const item = btn.closest('.faq-item');
|
||||||
|
const answer = item.querySelector('.faq-a');
|
||||||
|
const answerId = `faq-a-${index + 1}`;
|
||||||
|
if (answer) {
|
||||||
|
answer.id = answerId;
|
||||||
|
answer.setAttribute('role', 'region');
|
||||||
|
answer.setAttribute('aria-labelledby', `faq-q-${index + 1}`);
|
||||||
|
}
|
||||||
|
btn.id = `faq-q-${index + 1}`;
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
btn.setAttribute('aria-controls', answerId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: label each view section for screen-reader navigation
|
||||||
|
function initViewA11y() {
|
||||||
|
Object.entries(views).forEach(([key, el]) => {
|
||||||
|
if (el && viewLabels[key]) {
|
||||||
|
el.setAttribute('aria-label', viewLabels[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- U7: Creator Center ---
|
||||||
|
function renderCreatorCenter() {
|
||||||
|
renderCreatorRoles();
|
||||||
|
renderIncome();
|
||||||
|
renderSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCreatorRoles() {
|
||||||
|
const listEl = document.getElementById('creator-role-list');
|
||||||
|
const roles = state.roles.length > 0 ? state.roles : mockRoles;
|
||||||
|
listEl.innerHTML = roles
|
||||||
|
.map(
|
||||||
|
(role) => `
|
||||||
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.status === 'running' ? '运行中' : '已停止'}">
|
||||||
|
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||||
|
<div class="role-card__info">
|
||||||
|
<h3 class="role-card__name">${role.name}</h3>
|
||||||
|
<span class="role-card__status role-card__status--${role.status}">${role.status === 'running' ? '运行中' : '已停止'}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--small btn--outline" type="button" data-action="edit-role" data-role-id="${role.id}">编辑</button>
|
||||||
|
</article>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIncome() {
|
||||||
|
const income = state.income.balance > 0 ? state.income : mockIncome;
|
||||||
|
document.getElementById('income-balance').textContent = `¥ ${income.balance.toFixed(2)}`;
|
||||||
|
const listEl = document.getElementById('income-list');
|
||||||
|
if (income.records.length === 0) {
|
||||||
|
listEl.innerHTML = '<p class="income-empty">暂无流水记录</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listEl.innerHTML = income.records
|
||||||
|
.map(
|
||||||
|
(r) => `
|
||||||
|
<div class="income-record">
|
||||||
|
<div class="income-record__info">
|
||||||
|
<span class="income-record__role">${r.role}</span>
|
||||||
|
<span class="income-record__time">${r.time}</span>
|
||||||
|
</div>
|
||||||
|
<span class="income-record__amount">+¥${r.amount.toFixed(2)}</span>
|
||||||
|
</div>`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettings() {
|
||||||
|
document.getElementById('settings-name').value = state.creatorName || '';
|
||||||
|
document.getElementById('settings-library').value = state.libraryName === '我的 [XXX]' ? '' : state.libraryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchCenterTab(tab) {
|
||||||
|
activeCenterTab = tab;
|
||||||
|
document.querySelectorAll('.center-tab').forEach((t) => {
|
||||||
|
const isActive = t.dataset.centerTab === tab;
|
||||||
|
t.classList.toggle('active', isActive);
|
||||||
|
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.center-panel').forEach((p) => {
|
||||||
|
p.classList.toggle('active', p.id === `center-${tab}`);
|
||||||
|
});
|
||||||
|
const labels = { roles: '我的角色', income: '收入', settings: '我的' };
|
||||||
|
document.getElementById('center-tab-label').textContent = labels[tab] || '我的角色';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Creator form (U6: preserved from original) ---
|
||||||
|
function updateStep(index) {
|
||||||
|
steps.forEach((step, i) => {
|
||||||
|
step.classList.toggle('active', i === index);
|
||||||
|
});
|
||||||
|
dots.forEach((dot, i) => {
|
||||||
|
dot.classList.toggle('active', i === index);
|
||||||
|
});
|
||||||
|
currentStep = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateStep(index) {
|
||||||
|
const step = steps[index];
|
||||||
|
const inputs = step.querySelectorAll('input, textarea, select');
|
||||||
|
let valid = true;
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
if (!input.checkValidity()) {
|
||||||
|
valid = false;
|
||||||
|
input.reportValidity();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormData() {
|
||||||
|
const fd = new FormData(form);
|
||||||
|
const data = Object.fromEntries(fd.entries());
|
||||||
|
data.enableMemory = form.elements.enableMemory.checked;
|
||||||
|
data.enableTools = form.elements.enableTools.checked;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeYaml(value) {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
if (value.includes(':') || value.includes('#') || value.includes('\n') || value.includes('"')) {
|
||||||
|
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
return `"${escaped}"`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSoulMd(data) {
|
||||||
|
const personalityTags = data.personality
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' | ');
|
||||||
|
|
||||||
|
return `# Soul of ${data.displayName}
|
||||||
|
|
||||||
|
> Generated by Eternal AI — Hermes agent character soul.
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- **Name**: ${data.displayName}
|
||||||
|
- **Gender**: ${data.gender === 'unknown' ? '未指定' : data.gender}
|
||||||
|
- **Age**: ${data.age || '未指定'}
|
||||||
|
- **Role in your life**: ${data.relationship || '未指定'}
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
${data.background}
|
||||||
|
|
||||||
|
## Personality
|
||||||
|
|
||||||
|
${personalityTags}
|
||||||
|
|
||||||
|
## Speech Style
|
||||||
|
|
||||||
|
${data.speechStyle}
|
||||||
|
|
||||||
|
## Likes
|
||||||
|
|
||||||
|
${data.likes || 'None specified.'}
|
||||||
|
|
||||||
|
## Dislikes
|
||||||
|
|
||||||
|
${data.dislikes || 'None specified.'}
|
||||||
|
|
||||||
|
## Shared Memories
|
||||||
|
|
||||||
|
${data.memories || 'None specified.'}
|
||||||
|
|
||||||
|
## Secrets & Inner Voice
|
||||||
|
|
||||||
|
${data.secrets || 'None specified.'}
|
||||||
|
|
||||||
|
## First Greeting
|
||||||
|
|
||||||
|
${data.greeting}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Keep the memory alive.*
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateConfigYaml(data) {
|
||||||
|
const system = (data.systemPrompt || buildSystemPrompt(data)).trim();
|
||||||
|
const toolsBlock = data.enableTools
|
||||||
|
? `
|
||||||
|
tools:
|
||||||
|
- name: search_memory
|
||||||
|
description: Search long-term memory for context about the user.
|
||||||
|
- name: save_memory
|
||||||
|
description: Persist a new memory about the user or conversation.`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `# Hermes Agent Config
|
||||||
|
# Generated by Eternal AI for character: ${data.displayName}
|
||||||
|
|
||||||
|
agent:
|
||||||
|
id: ${data.agentId}
|
||||||
|
name: ${escapeYaml(data.displayName)}
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
model:
|
||||||
|
name: ${escapeYaml(data.model)}
|
||||||
|
temperature: ${data.temperature}
|
||||||
|
max_tokens: ${data.maxTokens}
|
||||||
|
|
||||||
|
system_prompt: |
|
||||||
|
${system.split('\n').map((line) => ` ${line}`).join('\n')}
|
||||||
|
|
||||||
|
memory:
|
||||||
|
enabled: ${data.enableMemory}
|
||||||
|
storage: local
|
||||||
|
recall_depth: 10${toolsBlock}
|
||||||
|
|
||||||
|
character:
|
||||||
|
soul_file: Soul.md
|
||||||
|
greeting: ${escapeYaml(data.greeting)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSystemPrompt(data) {
|
||||||
|
const tags = data.personality
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('、');
|
||||||
|
|
||||||
|
return `你是「${data.displayName}」,一个鲜活的角色。
|
||||||
|
|
||||||
|
基本设定:
|
||||||
|
- 性别:${data.gender === 'unknown' ? '未指定' : data.gender}
|
||||||
|
- 年龄:${data.age || '未指定'}
|
||||||
|
- 你与用户的关系:${data.relationship || '亲密关系'}
|
||||||
|
|
||||||
|
性格:${tags}
|
||||||
|
|
||||||
|
背景:
|
||||||
|
${data.background}
|
||||||
|
|
||||||
|
说话风格:
|
||||||
|
${data.speechStyle}
|
||||||
|
|
||||||
|
喜好:
|
||||||
|
${data.likes || '无特别说明'}
|
||||||
|
|
||||||
|
厌恶/底线:
|
||||||
|
${data.dislikes || '无特别说明'}
|
||||||
|
|
||||||
|
共同记忆:
|
||||||
|
${data.memories || '无特别说明'}
|
||||||
|
|
||||||
|
内心秘密(只在语气与选择中流露,不直接说出):
|
||||||
|
${data.secrets || '无'}
|
||||||
|
|
||||||
|
请始终保持角色一致性,用符合设定的语气与用户交流。你的目标不是完美回答问题,而是像一个真实、有记忆、有情绪的人一样陪伴对方。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSystemPromptPreview() {
|
||||||
|
const data = getFormData();
|
||||||
|
if (!systemPromptInput.value.trim()) {
|
||||||
|
systemPromptInput.dataset.auto = buildSystemPrompt(data);
|
||||||
|
} else {
|
||||||
|
delete systemPromptInput.dataset.auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish() {
|
||||||
|
if (!validateStep(currentStep)) return;
|
||||||
|
|
||||||
|
const data = getFormData();
|
||||||
|
if (!data.systemPrompt.trim()) {
|
||||||
|
data.systemPrompt = buildSystemPrompt(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedSoul = generateSoulMd(data);
|
||||||
|
generatedConfig = generateConfigYaml(data);
|
||||||
|
|
||||||
|
form.hidden = true;
|
||||||
|
resultPanel.hidden = false;
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview() {
|
||||||
|
previewCode.textContent = activePreview === 'soul' ? generatedSoul : generatedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function download(filename, content) {
|
||||||
|
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCreator() {
|
||||||
|
form.reset();
|
||||||
|
form.hidden = false;
|
||||||
|
resultPanel.hidden = true;
|
||||||
|
generatedSoul = '';
|
||||||
|
generatedConfig = '';
|
||||||
|
activePreview = 'soul';
|
||||||
|
updateStep(0);
|
||||||
|
updateTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTabs() {
|
||||||
|
document.querySelectorAll('.preview-tab').forEach((tab) => {
|
||||||
|
const isActive = tab.dataset.tab === activePreview;
|
||||||
|
tab.classList.toggle('active', isActive);
|
||||||
|
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Event delegation ---
|
||||||
|
// a11y: keyboard support for role cards (Enter / Space activates them)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
const roleCard = e.target.closest('.role-card');
|
||||||
|
if (roleCard && !e.target.closest('[data-action]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const roleId = roleCard.dataset.roleId;
|
||||||
|
renderRoleDetail(roleId);
|
||||||
|
showView('role-detail');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
// FAQ toggle (U4)
|
||||||
|
const faqBtn = e.target.closest('.faq-q');
|
||||||
|
if (faqBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleFaq(faqBtn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role card click (U2/U3)
|
||||||
|
const roleCard = e.target.closest('.role-card');
|
||||||
|
if (roleCard && !e.target.closest('[data-action]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const roleId = roleCard.dataset.roleId;
|
||||||
|
renderRoleDetail(roleId);
|
||||||
|
showView('role-detail');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab bar (U8)
|
||||||
|
const tabItem = e.target.closest('[data-tab-action]');
|
||||||
|
if (tabItem) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTabAction(tabItem.dataset.tabAction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = e.target.closest('[data-action], [data-tab], [data-download], [data-center-tab]');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const action = target.dataset.action;
|
||||||
|
|
||||||
|
// Center tab switching (U7)
|
||||||
|
if (target.dataset.centerTab) {
|
||||||
|
e.preventDefault();
|
||||||
|
switchCenterTab(target.dataset.centerTab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landing card: open characters (U1)
|
||||||
|
if (action === 'open-characters') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!state.isLoggedIn) {
|
||||||
|
switchAuthTab('login');
|
||||||
|
showView('auth');
|
||||||
|
} else if (state.isCreator) {
|
||||||
|
showView('creator-center');
|
||||||
|
renderCreatorCenter();
|
||||||
|
} else {
|
||||||
|
renderRoleLibrary();
|
||||||
|
showView('role-library');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'open-distill') {
|
||||||
|
e.preventDefault();
|
||||||
|
showView('distill');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'open-about') {
|
||||||
|
e.preventDefault();
|
||||||
|
showView('about');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'open-onboarding') {
|
||||||
|
e.preventDefault();
|
||||||
|
showView('onboarding');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'back') {
|
||||||
|
e.preventDefault();
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'back-to-library') {
|
||||||
|
e.preventDefault();
|
||||||
|
renderRoleLibrary();
|
||||||
|
showView('role-library');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'back-to-center') {
|
||||||
|
e.preventDefault();
|
||||||
|
showView('creator-center');
|
||||||
|
renderCreatorCenter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'pay') {
|
||||||
|
e.preventDefault();
|
||||||
|
payRole();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'pay-distill') {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('下单成功!请添加客服微信完成后续流程。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'contact-wechat') {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('客服微信号:EternalAI_Service');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'new-role') {
|
||||||
|
e.preventDefault();
|
||||||
|
resetCreator();
|
||||||
|
showView('creator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'edit-role') {
|
||||||
|
e.preventDefault();
|
||||||
|
resetCreator();
|
||||||
|
showView('creator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'logout') {
|
||||||
|
e.preventDefault();
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'download-avatar') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentRole) {
|
||||||
|
download(currentRole.name + '_avatar.png', '');
|
||||||
|
window.open(currentRole.avatar, '_blank');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creator form navigation
|
||||||
|
if (action === 'next') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (validateStep(currentStep) && currentStep < steps.length - 1) {
|
||||||
|
updateStep(currentStep + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'prev') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentStep > 0) {
|
||||||
|
updateStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'publish') {
|
||||||
|
e.preventDefault();
|
||||||
|
publish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'reset') {
|
||||||
|
e.preventDefault();
|
||||||
|
resetCreator();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab switching (auth tabs and preview tabs)
|
||||||
|
if (target.dataset.tab) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (target.closest('.auth-tabs')) {
|
||||||
|
switchAuthTab(target.dataset.tab);
|
||||||
|
} else {
|
||||||
|
activePreview = target.dataset.tab;
|
||||||
|
updateTabs();
|
||||||
|
renderPreview();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download buttons
|
||||||
|
if (target.dataset.download) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (target.dataset.download === 'soul') {
|
||||||
|
download('Soul.md', generatedSoul);
|
||||||
|
} else {
|
||||||
|
download('config.yaml', generatedConfig);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Auth form submissions ---
|
||||||
|
document.querySelectorAll('.auth-form').forEach((authForm) => {
|
||||||
|
authForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validatePasswordMatch(authForm)) return;
|
||||||
|
const formData = new FormData(authForm);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
login(data.account);
|
||||||
|
|
||||||
|
// After login, go to role library or creator center
|
||||||
|
if (state.isCreator) {
|
||||||
|
showView('creator-center');
|
||||||
|
renderCreatorCenter();
|
||||||
|
} else {
|
||||||
|
renderRoleLibrary();
|
||||||
|
showView('role-library');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Settings form (U7) ---
|
||||||
|
document.getElementById('settings-form').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
state.creatorName = data.creatorName || '';
|
||||||
|
state.libraryName = data.libraryName || '我的 [XXX]';
|
||||||
|
saveState();
|
||||||
|
updateLandingCard();
|
||||||
|
alert('设置已保存');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Withdraw form (U7) ---
|
||||||
|
document.getElementById('withdraw-form').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
const amount = parseFloat(data.amount);
|
||||||
|
if (amount > state.income.balance && amount > mockIncome.balance) {
|
||||||
|
alert('提现金额超过可提现余额');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert(`提现申请已提交:${data.method === 'wechat' ? '微信' : '支付宝'} ¥${amount.toFixed(2)}\n平台负责人将手动审核转账。`);
|
||||||
|
e.target.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Creator form input ---
|
||||||
|
form.addEventListener('input', () => {
|
||||||
|
updateSystemPromptPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Initialize ---
|
||||||
|
updateStep(0);
|
||||||
|
updateSystemPromptPreview();
|
||||||
|
updateLandingCard();
|
||||||
|
updateTabBar('landing');
|
||||||
|
initFaqA11y();
|
||||||
|
initViewA11y();
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,375 @@
|
||||||
|
# Eternal AI 完整功能与链接路由规划
|
||||||
|
|
||||||
|
**Type:** feat
|
||||||
|
**Date:** 2026-06-20
|
||||||
|
**Origin:** `Eternal_AI_PRD_v1.docx`(微信小程序 PRD v1.0)
|
||||||
|
**Target repo:** EternalAI
|
||||||
|
**Status:** active
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Summary
|
||||||
|
|
||||||
|
本规划依据 `Eternal_AI_PRD_v1.docx` 对现有 HTML5 落地页进行补全,核心目标是:
|
||||||
|
|
||||||
|
1. 让首页四个入口(我的 XXX、蒸馏前任、关于 Eternal AI、申请入驻)分别指向 PRD 定义的独立页面,而不是全部打开同一个人设蒸馏表单。
|
||||||
|
2. 补齐 PRD 中缺失的 4 个页面:角色库页(P2)、角色详情页(P3)、关于 Eternal AI 页(P5)、创作者入驻页(P6)。
|
||||||
|
3. 将现有的人设蒸馏表单重新定位到「创作者管理中心 - 角色编辑」(P7),保留 Soul.md / config.yaml 生成功能。
|
||||||
|
4. 增加底部 tabBar(首页 / 蒸馏前任 / 我的专属创作者),与 PRD 导航结构一致。
|
||||||
|
|
||||||
|
当前已实现的页面:首页(P1)、登录/注册视图、蒸馏前任服务页(P4)、人设蒸馏表单(待重新定位)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Problem Frame
|
||||||
|
|
||||||
|
现有实现把三个完全不同的用户意图塞进了同一个表单:
|
||||||
|
|
||||||
|
- **普通用户想找角色库** → 打开了登录/注册(已修正,但登录后无角色库可去)。
|
||||||
|
- **普通用户想购买蒸馏前任服务** → 打开了正确页面(P4)。
|
||||||
|
- **创作者想入驻** → 打开了人设蒸馏表单(错误:PRD 要求 P6 仅展示合作说明与微信二维码,不填表单)。
|
||||||
|
- **关于 Eternal AI** → 仅有一个空锚点 `#about`,无实际页面。
|
||||||
|
|
||||||
|
此外,PRD 定义的角色库(P2)、角色详情(P3)、创作者管理中心(P7)均未实现,导致用户登录后没有内容,创作者也没有入口管理角色和收入。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Requirements Traceability
|
||||||
|
|
||||||
|
| PRD 页面 | 访问权限 | 入口 | 当前状态 | 规划动作 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| P1 首页 | 所有人 | 打开站点 | 已实现 | 保持,调整链接 |
|
||||||
|
| P2 角色库页 | 已登录 + 已绑定创作者 | 首页「我的 XXX」 | 缺失 | 新增 |
|
||||||
|
| P3 角色详情页 | 已登录用户 | P2 角色卡片 | 缺失 | 新增 |
|
||||||
|
| P4 蒸馏前任页 | 所有人 | 首页「蒸馏前任」/ tabBar | 已实现 | 保持,补充底部 tabBar 入口 |
|
||||||
|
| P5 关于 Eternal AI 页 | 所有人 | 首页底部「关于 Eternal AI」 | 缺失 | 新增 |
|
||||||
|
| P6 创作者入驻页 | 所有人 | 首页底部「申请入驻」 | 错误指向 creator 表单 | 改为微信联系说明页 |
|
||||||
|
| P7 创作者管理中心 | 创作者账号登录后 | tabBar「管理」 | 缺失 | 新增,复用现有 creator 表单作为「角色编辑」 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Link Rationality(首页入口调整说明)
|
||||||
|
|
||||||
|
### 4.1 我的 XXX → 登录 → 角色库(P2)
|
||||||
|
|
||||||
|
**为什么这样设计:**
|
||||||
|
- PRD 中「我的 XXX」是角色库入口,而角色库需要登录后才能访问。
|
||||||
|
- 未登录用户点击后应先进入登录/注册;登录后根据是否绑定创作者决定展示内容。
|
||||||
|
- 登录态由前端本地模拟(localStorage 标记),后续由后端接管。
|
||||||
|
|
||||||
|
**状态分支:**
|
||||||
|
- 未登录 → 登录/注册视图。
|
||||||
|
- 已登录 + 已绑定创作者 → P2 角色库(显示该创作者上架的角色)。
|
||||||
|
- 已登录 + 未绑定创作者 → P2 角色库占位态,提示「你还没有绑定专属创作者」。
|
||||||
|
|
||||||
|
### 4.2 蒸馏前任 → P4 蒸馏前任服务页
|
||||||
|
|
||||||
|
**为什么这样设计:**
|
||||||
|
- 与 PRD 一致,面向情感类用户的自营服务。
|
||||||
|
- 保持现有 P4 页面结构:服务介绍、流程、价格、下单、客服微信、分润说明。
|
||||||
|
|
||||||
|
### 4.3 关于 Eternal AI → P5 关于页
|
||||||
|
|
||||||
|
**为什么这样设计:**
|
||||||
|
- PRD 明确这是建立女性用户信任的页面,需要展示平台简介、连接方式说明、FAQ(安全/隐私/账号/情感)。
|
||||||
|
- 当前空锚点 `#about` 无法承载任何信息,必须新建独立视图。
|
||||||
|
|
||||||
|
### 4.4 我是创作者,申请入驻 → P6 创作者入驻页
|
||||||
|
|
||||||
|
**为什么这样设计:**
|
||||||
|
- PRD 明确 P6「不做表单提交,直接走微信私聊沟通」。
|
||||||
|
- 现有的人设蒸馏表单是创作者入驻后的生产工具,不是入驻申请本身。
|
||||||
|
- 因此 footer 入口应改为展示合作模式、分润比例、负责人微信二维码的引导页。
|
||||||
|
|
||||||
|
### 4.5 底部 tabBar
|
||||||
|
|
||||||
|
**为什么这样设计:**
|
||||||
|
- PRD 底部导航为「首页 / 蒸馏前任 / 我的专属创作者」。
|
||||||
|
- 在 HTML5 站点中用固定底部 tabBar 模拟小程序导航,让 P4 和 P2/P7 都有全局入口。
|
||||||
|
- tabBar 在首页、蒸馏前任、角色库/创作者中心之间切换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Implementation Units
|
||||||
|
|
||||||
|
### U1. 调整「我的 XXX」链路:未登录进登录页,已登录进角色库
|
||||||
|
|
||||||
|
**Goal:** 让首页「我的 XXX」卡片根据登录态正确分流。
|
||||||
|
|
||||||
|
**Requirements:** PRD P1 板块三、P2 条件逻辑。
|
||||||
|
|
||||||
|
**Dependencies:** U2(角色库页必须先存在)。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 在 `app.js` 增加 `authState` 对象:`{ isLoggedIn: bool, boundCreator: object|null }`。
|
||||||
|
- 点击「我的 XXX」时:
|
||||||
|
- 未登录 → `showView('auth')`,登录成功后写入 `authState` 并跳转 P2。
|
||||||
|
- 已登录 → 直接 `showView('role-library')`。
|
||||||
|
- 登录/注册提交不再 `alert`,而是更新状态并跳转。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 未登录点击「我的 XXX」→ 显示登录页。
|
||||||
|
- 登录成功后点击「我的 XXX」→ 显示角色库。
|
||||||
|
- 登录后 `boundCreator` 为空 → 角色库显示「未绑定创作者」提示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U2. 新增角色库页(P2)
|
||||||
|
|
||||||
|
**Goal:** 展示创作者上架的所有 AI 角色。
|
||||||
|
|
||||||
|
**Requirements:** PRD P2。
|
||||||
|
|
||||||
|
**Dependencies:** U1。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`(新增 `#role-library` view)
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 新增 `view--role-library`,顶部显示创作者自定义名称(如「云朵的后宫」)。
|
||||||
|
- 角色卡片列表:头像、名字、简短简介、价格。
|
||||||
|
- 点击卡片 → P3 角色详情。
|
||||||
|
- 空态:未绑定创作者时显示提示文案。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 角色列表按 PRD 字段渲染。
|
||||||
|
- 点击卡片正确跳转角色详情。
|
||||||
|
- 未绑定创作者时显示空态提示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U3. 新增角色详情页(P3)
|
||||||
|
|
||||||
|
**Goal:** 展示单个角色详情并模拟订阅/二维码流程。
|
||||||
|
|
||||||
|
**Requirements:** PRD P3。
|
||||||
|
|
||||||
|
**Dependencies:** U2。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`(新增 `#role-detail` view)
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 新增 `view--role-detail`,包含:角色大图、名字、详细介绍、价格、付款按钮。
|
||||||
|
- 点击「付款」后切换到已付款态:显示微信专属二维码、可下载头像、引导文案。
|
||||||
|
- 由于无真实支付接口,付款按钮先切换本地状态并展示二维码占位图。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 付款前态显示价格与按钮。
|
||||||
|
- 点击付款后切换到已付款态,显示二维码和头像下载。
|
||||||
|
- 返回按钮回到角色库。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U4. 新增关于 Eternal AI 页(P5)
|
||||||
|
|
||||||
|
**Goal:** 建立用户信任,解答安全、隐私、账号、情感类常见问题。
|
||||||
|
|
||||||
|
**Requirements:** PRD P5。
|
||||||
|
|
||||||
|
**Dependencies:** 无。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`(新增 `#about` view)
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 新增 `view--about`,结构:平台简介、连接方式说明(Claw / iLink)、FAQ 折叠面板。
|
||||||
|
- FAQ 分类:安全类、隐私类、账号类、情感类。
|
||||||
|
- 首页底部「关于 Eternal AI」改为 `data-action="open-about"`。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 点击 footer「关于 Eternal AI」打开 P5。
|
||||||
|
- FAQ 可展开/收起。
|
||||||
|
- 返回按钮回到首页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U5. 重构创作者入驻页(P6)为微信联系引导
|
||||||
|
|
||||||
|
**Goal:** 将 footer「申请入驻」从 creator 表单改为 PRD 定义的入驻说明页。
|
||||||
|
|
||||||
|
**Requirements:** PRD P6。
|
||||||
|
|
||||||
|
**Dependencies:** 无。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`(新增 `#creator-onboarding` view,或复用/替换原 footer 目标)
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 新增 `view--creator-onboarding`:入驻说明、合作模式(20% 平台 + 80% 创作者)、负责人微信二维码/微信号。
|
||||||
|
- 不提交表单,仅展示联系方式。
|
||||||
|
- footer「我是创作者,申请入驻」改为 `data-action="open-creator-onboarding"`。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 点击 footer「申请入驻」打开 P6 说明页。
|
||||||
|
- 页面包含微信二维码和分润说明。
|
||||||
|
- 返回按钮回到首页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U6. 将现有人设蒸馏表单重新定位为「创作者管理中心 - 角色编辑」
|
||||||
|
|
||||||
|
**Goal:** 保留 Soul.md / config.yaml 生成功能,但仅作为创作者后台工具。
|
||||||
|
|
||||||
|
**Requirements:** PRD P7 Tab 1 角色管理页;同时满足用户原始需求(生成 Hermes agent 配置文件)。
|
||||||
|
|
||||||
|
**Dependencies:** U5。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 当前 `#creator` view 从「landing footer 入口」改为「creator center 内部编辑页」。
|
||||||
|
- 入口文案调整:header 从「Eternal AI / 人设蒸馏器」改为「角色编辑 / 生成配置文件」。
|
||||||
|
- 保留 4 步表单和下载逻辑。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 从创作者管理中心点击角色进入编辑页时,表单正常显示。
|
||||||
|
- 提交后仍能生成并下载 Soul.md + config.yaml。
|
||||||
|
- 返回按钮回到创作者管理中心的角色列表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U7. 新增创作者管理中心(P7)
|
||||||
|
|
||||||
|
**Goal:** 为创作者提供角色管理、收入、账户设置入口。
|
||||||
|
|
||||||
|
**Requirements:** PRD P7。
|
||||||
|
|
||||||
|
**Dependencies:** U5, U6。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`(新增 `#creator-center` view)
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 新增 `view--creator-center`,内部三个 tab:我的角色 / 收入 / 我的。
|
||||||
|
- **我的角色:** 角色列表(头像、名字、状态),点击角色进入 U6 的编辑页,新增角色也进入同一表单。
|
||||||
|
- **收入:** 可提现余额、流水明细、提现申请表单(收款方式 + 金额)。
|
||||||
|
- **我的:** 创作者名字、自定义角色库名称(即首页「我的 XXX」显示文字)、退出登录。
|
||||||
|
- 该中心仅在 `authState` 标记为创作者时从 tabBar「管理」进入;普通用户该 tab 指向角色库或提示申请创作者。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 三个 tab 可切换。
|
||||||
|
- 点击角色进入编辑表单。
|
||||||
|
- 自定义角色库名称保存后,首页「我的 XXX」同步更新。
|
||||||
|
- 收入页显示余额与流水,提现表单可提交(前端模拟)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U8. 新增底部 tabBar 导航
|
||||||
|
|
||||||
|
**Goal:** 模拟 PRD 的小程序底部导航:首页 / 蒸馏前任 / 我的专属创作者。
|
||||||
|
|
||||||
|
**Requirements:** PRD P1 底部导航栏。
|
||||||
|
|
||||||
|
**Dependencies:** U2, U4, U7。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `index.html`
|
||||||
|
- `styles.css`
|
||||||
|
- `app.js`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 在 `index.html` 底部新增 `.tab-bar` 固定元素。
|
||||||
|
- 三个 tab:首页 → P1;蒸馏前任 → P4;我的专属创作者 → 未登录进登录页,已登录普通用户进 P2,已登录创作者进 U7 创作者管理中心。
|
||||||
|
- tabBar 在当前页面对应 tab 上高亮。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 三个 tab 正确切换视图。
|
||||||
|
- 高亮状态与当前视图一致。
|
||||||
|
- 未登录时点击第三个 tab 先进登录页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### U9. 统一视图路由与状态管理
|
||||||
|
|
||||||
|
**Goal:** 让多视图切换更可靠,便于后续接入真实后端。
|
||||||
|
|
||||||
|
**Requirements:** 全局路由一致性。
|
||||||
|
|
||||||
|
**Dependencies:** U1–U8。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `app.js`
|
||||||
|
- `index.html`
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- 维护 `views` 字典,所有视图统一 `showView(viewId)` 切换。
|
||||||
|
- `authState` 和 `creatorState` 集中到顶部,提供 `login()`, `logout()`, `bindCreator()` 方法。
|
||||||
|
- 每次视图切换时滚动到顶部,并更新 tabBar 高亮。
|
||||||
|
- 保留 URL hash 与当前视图同步(可选,便于刷新后定位)。
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
- 任意返回按钮都能回到正确上一页或首页。
|
||||||
|
- 刷新页面后仍保持登录态(localStorage)。
|
||||||
|
- 视图切换无 JavaScript 报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Key Technical Decisions
|
||||||
|
|
||||||
|
| 决策 | 选项 | 选择 | 理由 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 登录态存储 | localStorage / sessionStorage / 仅内存 | localStorage | 单页应用刷新后保持状态,后续后端接管时替换为 token 即可。 |
|
||||||
|
| 支付流程 | 真实支付接口 / 前端模拟 | 前端模拟 | PRD 附录列明「自动支付接口暂不开发」,一期仅做 UI 状态切换。 |
|
||||||
|
| 角色数据 | 硬编码 mock / localStorage / 后端 | 硬编码 mock + 可扩展结构 | 一期先展示前端,数据结构按后端接口预留字段。 |
|
||||||
|
| 创作者表单定位 | 删除 / 保留在创作者中心 | 保留在创作者中心 | 满足用户原始需求:生成 Soul.md 与 config.yaml。 |
|
||||||
|
| tabBar 实现 | 固定底部栏 / 无 tabBar | 固定底部栏 | 与 PRD 一致,也弥补 HTML5 缺少小程序原生导航的问题。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Scope Boundaries
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
- 首页四个入口的路由修正。
|
||||||
|
- P2、P3、P5、P6、P7 的页面结构与交互。
|
||||||
|
- 底部 tabBar。
|
||||||
|
- 前端模拟的登录态、创作者态、支付态。
|
||||||
|
|
||||||
|
### Deferred to Follow-Up Work
|
||||||
|
- 真实后端 API 对接(登录、支付、角色 CRUD、收入流水)。
|
||||||
|
- 文件上传(蒸馏前任聊天记录),PRD 已明确一期走微信人工指导。
|
||||||
|
- 自动提现审核与转账,PRD 已明确一期手动处理。
|
||||||
|
- 运营后台。
|
||||||
|
- 创作者页面颜色主题自定义。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Risks & Dependencies
|
||||||
|
|
||||||
|
| 风险 | 影响 | 缓解 |
|
||||||
|
|---|---|---|
|
||||||
|
| PRD 面向微信小程序,而当前是 HTML5 站点,部分交互(如微信扫码、小程序 tabBar)需要重新设计。 | 中 | 用 HTML5 可实现的等价交互替代:tabBar 用固定底部栏、扫码用二维码占位图、微信支付用模拟按钮。 |
|
||||||
|
| 创作者管理中心把现有人设蒸馏表单作为角色编辑页,表单字段可能与 PRD P7 的「角色管理页」字段不完全一致。 | 低 | 在 U6 中明确表单就是角色编辑工具,PRD P7 的其他字段(价格、用量、二维码)在角色列表中展示。 |
|
||||||
|
| 缺少真实后端,登录/支付状态容易在刷新或切换设备时丢失。 | 低 | 一期用 localStorage,并在计划中标注后续对接点。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Open Questions
|
||||||
|
|
||||||
|
1. 创作者入驻(P6)的负责人微信号和二维码是否已确定?需要真实素材占位还是先用示例图?
|
||||||
|
2. 角色库页(P2)的示例角色数据是否需要用户提供,还是先用 2–3 个虚构角色填充?
|
||||||
|
3. 首页「我的 XXX」中的 XXX 默认文案是否保留「我的 [XXX]」还是给一个默认占位如「我的角色库」?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Sources & Research
|
||||||
|
|
||||||
|
- `Eternal_AI_PRD_v1.docx` — 产品需求文档,定义 7 个页面及交互逻辑。
|
||||||
|
- 当前代码:`index.html`、`app.js`、`styles.css` — 现有单页应用结构与视图切换方式。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
651
index.html
651
index.html
|
|
@ -1,67 +1,600 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EternalAI Application</title>
|
<title>Eternal AI - 在记忆与陪伴中,遇见更懂你的 AI</title>
|
||||||
<style>
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
body {
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
font-family: Arial, sans-serif;
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||||
margin: 0;
|
<link rel="stylesheet" href="styles.css?v=7" />
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #e8f5e8;
|
|
||||||
border: 1px solid #4caf50;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<a href="#landing" class="skip-link">跳到主内容</a>
|
||||||
<h1>Welcome to EternalAI</h1>
|
<main class="app">
|
||||||
<p>The application is running successfully!</p>
|
<!-- P1: Landing View -->
|
||||||
|
<section id="landing" class="view view--landing active">
|
||||||
<div id="status" class="status">
|
<div class="cards">
|
||||||
Status: <span id="status-text">Checking...</span><br>
|
<article class="card card--characters" data-action="open-characters">
|
||||||
Timestamp: <span id="timestamp"></span>
|
<div class="card__frame" aria-hidden="true"></div>
|
||||||
|
<div class="card__content">
|
||||||
|
<h2 class="card__title" id="library-name">我的 [XXX]</h2>
|
||||||
|
<p class="card__desc" id="characters-desc">登录后管理你的角色</p>
|
||||||
|
<button class="btn btn--outline" type="button" id="characters-btn">登录 / 注册</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card card--distill" data-action="open-distill">
|
||||||
|
<div class="card__frame" aria-hidden="true"></div>
|
||||||
|
<div class="card__content">
|
||||||
|
<h2 class="card__title">蒸馏前任</h2>
|
||||||
|
<p class="card__desc">留住你心里的 ta</p>
|
||||||
|
<button class="btn btn--primary" type="button">
|
||||||
|
开始蒸馏
|
||||||
|
<span class="btn__arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="landing-footer">
|
||||||
|
<a href="#" class="footer-link" data-action="open-about">关于 Eternal AI →</a>
|
||||||
|
<a href="#" class="footer-link" data-action="open-onboarding">我是创作者,申请入驻 →</a>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Auth View (Login / Register) -->
|
||||||
|
<section id="auth" class="view view--auth">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">Eternal AI</span>
|
||||||
|
<span class="creator-brand__step">登录 / 注册</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
<div>
|
</header>
|
||||||
<h3>Available Endpoints:</h3>
|
|
||||||
<ul>
|
<div class="auth-tabs" role="tablist" aria-label="登录与注册">
|
||||||
<li><a href="/api/status">/api/status</a> - API Status</li>
|
<button class="auth-tab active" type="button" role="tab" aria-selected="true" data-tab="login">登录</button>
|
||||||
<li><a href="/health">/health</a> - Health Check</li>
|
<button class="auth-tab" type="button" role="tab" aria-selected="false" data-tab="register">注册</button>
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
|
<form id="login-form" class="auth-form active" autocomplete="on" data-form="login">
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">手机号 / 用户名</span>
|
||||||
|
<input class="field__input" name="account" type="text" autocomplete="username" placeholder="请输入手机号或用户名" required />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">密码</span>
|
||||||
|
<input class="field__input" name="password" type="password" autocomplete="current-password" placeholder="请输入密码" required />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="submit">登录</button>
|
||||||
<script>
|
</div>
|
||||||
fetch('/api/status')
|
<p class="auth-hint">登录后可查看角色库、管理已订阅的角色。</p>
|
||||||
.then(response => response.json())
|
</form>
|
||||||
.then(data => {
|
|
||||||
document.getElementById('status-text').textContent = 'Running';
|
<form id="register-form" class="auth-form" autocomplete="on" data-form="register">
|
||||||
document.getElementById('timestamp').textContent = data.timestamp;
|
<div class="field-group">
|
||||||
console.log('EternalAI Status:', data);
|
<label class="field">
|
||||||
})
|
<span class="field__label">手机号 / 用户名</span>
|
||||||
.catch(error => {
|
<input class="field__input" name="account" type="text" autocomplete="username" placeholder="设置登录账号" required />
|
||||||
document.getElementById('status-text').textContent = 'Error';
|
</label>
|
||||||
console.error('Error fetching status:', error);
|
<label class="field">
|
||||||
});
|
<span class="field__label">密码</span>
|
||||||
</script>
|
<input class="field__input" name="password" type="password" autocomplete="new-password" placeholder="设置密码" required minlength="6" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">确认密码</span>
|
||||||
|
<input class="field__input" name="confirmPassword" type="password" autocomplete="new-password" placeholder="再次输入密码" required />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="submit">注册</button>
|
||||||
|
</div>
|
||||||
|
<p class="auth-hint">注册即代表同意《用户协议》和《隐私政策》。</p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P2: Role Library View -->
|
||||||
|
<section id="role-library" class="view view--role-library">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name" id="library-title">我的角色库</span>
|
||||||
|
<span class="creator-brand__step">选择一个角色开始对话</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="role-list" id="role-list" aria-live="polite" aria-label="角色列表"></div>
|
||||||
|
|
||||||
|
<div class="empty-state" id="library-empty" hidden>
|
||||||
|
<p class="empty-state__text">你还没有绑定专属创作者</p>
|
||||||
|
<p class="empty-state__hint">通过创作者的专属链接进入即可绑定</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P3: Role Detail View -->
|
||||||
|
<section id="role-detail" class="view view--role-detail">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back-to-library" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name" id="detail-name">角色详情</span>
|
||||||
|
<span class="creator-brand__step">了解 ta 并订阅</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="detail-page" id="detail-page">
|
||||||
|
<div class="detail-hero" id="detail-hero"></div>
|
||||||
|
<h2 class="detail-title" id="detail-role-name"></h2>
|
||||||
|
<p class="detail-desc" id="detail-role-desc"></p>
|
||||||
|
<div class="detail-price" id="detail-price"></div>
|
||||||
|
|
||||||
|
<div class="form-actions" id="detail-actions-pre">
|
||||||
|
<button class="btn btn--primary btn--block" type="button" data-action="pay">立即订阅</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-paid" id="detail-paid" hidden>
|
||||||
|
<div class="detail-qr" id="detail-qr"></div>
|
||||||
|
<p class="detail-paid__hint">扫码添加后,请将下方头像保存并设置为该联系人的备注头像,获得更完整的体验</p>
|
||||||
|
<div class="detail-avatar" id="detail-avatar"></div>
|
||||||
|
<button class="btn btn--outline btn--block" type="button" data-action="download-avatar">下载角色头像</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P4: Distill Ex View -->
|
||||||
|
<section id="distill" class="view view--distill-page">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">Eternal AI</span>
|
||||||
|
<span class="creator-brand__step">蒸馏前任</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="distill-page">
|
||||||
|
<a href="#" class="distill-quick" data-action="contact-wechat">没耐心?直接加微信定制沟通 →</a>
|
||||||
|
|
||||||
|
<div class="distill-section">
|
||||||
|
<h3 class="distill-section__title">什么是蒸馏前任?</h3>
|
||||||
|
<p class="distill-section__text">把你们曾经的聊天记录、语音习惯、相处细节交给我们,技术团队会将其蒸馏成一个可对话的 AI 前任。ta 会记得你们的暗号、说话节奏,甚至那些只有你们懂的小情绪。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="distill-section">
|
||||||
|
<h3 class="distill-section__title">适合谁?</h3>
|
||||||
|
<ul class="distill-list">
|
||||||
|
<li>想好好告别,却还没说完话的人</li>
|
||||||
|
<li>希望把一段关系以安全方式封存的人</li>
|
||||||
|
<li>想借 AI 完成自我疗愈、练习表达的人</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="distill-section">
|
||||||
|
<h3 class="distill-section__title">服务流程</h3>
|
||||||
|
<ol class="distill-steps">
|
||||||
|
<li><span>①</span> 下单付款</li>
|
||||||
|
<li><span>②</span> 客服指导导出聊天记录</li>
|
||||||
|
<li><span>③</span> 上传至指定云盘</li>
|
||||||
|
<li><span>④</span> 技术人员处理蒸馏</li>
|
||||||
|
<li><span>⑤</span> 完成后获得专属二维码和头像</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="distill-price">
|
||||||
|
<span class="distill-price__label">标准版</span>
|
||||||
|
<span class="distill-price__value">¥ 199</span>
|
||||||
|
<span class="distill-price__unit">/ 次</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="button" data-action="pay-distill">立即下单</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="distill-note">
|
||||||
|
<p>付款后请添加客服微信,我们将一对一指导你完成聊天记录导出与上传。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="distill-revenue">
|
||||||
|
<p>创作者可推广蒸馏前任服务获得分润,具体比例请在创作者管理中心配置。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P5: About View -->
|
||||||
|
<section id="about" class="view view--about">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">关于 Eternal AI</span>
|
||||||
|
<span class="creator-brand__step">安全 · 隐私 · 信任</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="about-page">
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">平台简介</h3>
|
||||||
|
<p class="about-section__text">Eternal AI 是一个 AI 陪伴平台,帮助创作者将人设设定部署为可对话的 AI 角色,让用户在微信中与 ta 交流。我们致力于在记忆与陪伴中,遇见更懂你的 AI。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">连接方式说明</h3>
|
||||||
|
<p class="about-section__text">Eternal AI 基于微信 Claw 开放协议(iLink),非破解、非抓包,在微信官方授权范围内实现 AI 联系人接入。你的微信账号安全不受影响。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">安全类</h3>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">你们能看到我其他的微信聊天吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不能。我们只能看到你与 AI 角色的对话,无法访问你的其他聊天、朋友圈或通讯录。</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">会不会偷偷读我的朋友圈和通讯录?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不会。我们仅通过 Claw 协议与 AI 联系人通信,不具备读取你朋友圈或通讯录的能力。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">隐私类</h3>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">我和 AI 说的话会被保存吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>对话内容会保存在你的专属记忆库中,用于 AI 角色记住你们的对话上下文。仅你和你的 AI 角色可访问,我们不会人工查看。</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">蒸馏前任要上传聊天记录,这些数据安全吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>上传的聊天记录仅用于训练你的专属 AI 前任,处理完成后原始数据将被删除。我们承诺不会将你的数据用于其他用途。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">账号类</h3>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">用了会不会封我的微信号?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不会。Claw 协议在微信官方授权范围内运行,与正常添加联系人无异,不存在封号风险。</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">扫码之后对方能控制我的微信吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不能。AI 角色只是一个普通的微信联系人,只能与你收发消息,无法控制你的账号。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-section">
|
||||||
|
<h3 class="about-section__title">情感类</h3>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">我聊的内容会有人看吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不会有人工查看你的对话内容。除非你主动联系客服反馈问题,我们才会根据你的授权查看相关记录。</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="faq-item">
|
||||||
|
<button class="faq-q" type="button">你们会把我的对话内容拿去训练 AI 吗?<span class="faq-icon">+</span></button>
|
||||||
|
<div class="faq-a"><p>不会。你的对话内容仅用于你自己的 AI 角色的记忆,绝不会用于训练其他模型或分享给第三方。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P6: Creator Onboarding View -->
|
||||||
|
<section id="onboarding" class="view view--onboarding">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">创作者入驻</span>
|
||||||
|
<span class="creator-brand__step">部署 AI 角色,私域变现</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="onboarding-page">
|
||||||
|
<div class="onboarding-section">
|
||||||
|
<h3 class="onboarding-section__title">平台能为创作者提供什么</h3>
|
||||||
|
<ul class="onboarding-list">
|
||||||
|
<li>部署 AI 角色:提交人设设定,平台负责技术部署</li>
|
||||||
|
<li>资金中转:平台处理用户付款,按比例结算给创作者</li>
|
||||||
|
<li>私域变现:通过专属链接绑定粉丝,持续获得订阅收入</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="onboarding-section">
|
||||||
|
<h3 class="onboarding-section__title">合作模式</h3>
|
||||||
|
<p class="onboarding-section__text">创作者提交角色人设,平台部署上线。用户付款后,平台抽佣 20%,80% 转给创作者。结算周期为月结。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="onboarding-section onboarding-contact">
|
||||||
|
<h3 class="onboarding-section__title">联系入驻</h3>
|
||||||
|
<div class="onboarding-qr" id="onboarding-qr"></div>
|
||||||
|
<p class="onboarding-wechat">微信扫码添加负责人</p>
|
||||||
|
<p class="onboarding-wechat-id">微信号:EternalAI_Official</p>
|
||||||
|
<p class="onboarding-note">添加时请备注「创作者入驻」,我们会一对一沟通合作细节。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- P7: Creator Center View -->
|
||||||
|
<section id="creator-center" class="view view--creator-center">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">创作者管理中心</span>
|
||||||
|
<span class="creator-brand__step" id="center-tab-label">我的角色</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="center-tabs" role="tablist" aria-label="创作者管理中心">
|
||||||
|
<button class="center-tab active" type="button" role="tab" aria-selected="true" data-center-tab="roles">我的角色</button>
|
||||||
|
<button class="center-tab" type="button" role="tab" aria-selected="false" data-center-tab="income">收入</button>
|
||||||
|
<button class="center-tab" type="button" role="tab" aria-selected="false" data-center-tab="settings">我的</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Roles -->
|
||||||
|
<div class="center-panel active" id="center-roles">
|
||||||
|
<div class="role-list" id="creator-role-list" aria-live="polite" aria-label="我的角色列表"></div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="button" data-action="new-role">+ 新建角色</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Income -->
|
||||||
|
<div class="center-panel" id="center-income">
|
||||||
|
<div class="income-balance">
|
||||||
|
<span class="income-balance__label">可提现余额</span>
|
||||||
|
<span class="income-balance__value" id="income-balance">¥ 0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="income-section">
|
||||||
|
<h3 class="income-section__title">流水明细</h3>
|
||||||
|
<div class="income-list" id="income-list" aria-live="polite" aria-label="收入流水">
|
||||||
|
<p class="income-empty">暂无流水记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="income-section">
|
||||||
|
<h3 class="income-section__title">申请提现</h3>
|
||||||
|
<form id="withdraw-form" class="withdraw-form" autocomplete="off">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">收款方式</span>
|
||||||
|
<select class="field__input" name="method" required>
|
||||||
|
<option value="wechat">微信</option>
|
||||||
|
<option value="alipay">支付宝</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">提现金额(¥)</span>
|
||||||
|
<input class="field__input" name="amount" type="number" min="1" step="0.01" placeholder="请输入金额" required />
|
||||||
|
</label>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="submit">提交提现申请</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Settings -->
|
||||||
|
<div class="center-panel" id="center-settings">
|
||||||
|
<form id="settings-form" class="settings-form" autocomplete="off">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">创作者名字</span>
|
||||||
|
<input class="field__input" name="creatorName" type="text" placeholder="你的笔名" id="settings-name" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">角色库名称(首页「我的 XXX」显示的文字)</span>
|
||||||
|
<input class="field__input" name="libraryName" type="text" placeholder="如:云朵的后宫" id="settings-library" />
|
||||||
|
</label>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary btn--block" type="submit">保存设置</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--ghost btn--block" type="button" data-action="logout">退出登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Creator Role Edit View (formerly character creator) -->
|
||||||
|
<section id="creator" class="view view--creator">
|
||||||
|
<header class="creator-header">
|
||||||
|
<button class="icon-btn" type="button" data-action="back-to-center" aria-label="返回">←</button>
|
||||||
|
<div class="creator-brand">
|
||||||
|
<span class="creator-brand__name">角色编辑</span>
|
||||||
|
<span class="creator-brand__step">生成配置文件</span>
|
||||||
|
</div>
|
||||||
|
<div class="stepper" aria-hidden="true">
|
||||||
|
<span class="stepper__dot active"></span>
|
||||||
|
<span class="stepper__dot"></span>
|
||||||
|
<span class="stepper__dot"></span>
|
||||||
|
<span class="stepper__dot"></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="character-form" class="creator-form" autocomplete="off">
|
||||||
|
<!-- Step 1: Identity -->
|
||||||
|
<fieldset class="form-step active" data-step="0">
|
||||||
|
<legend class="form-step__legend">基础身份</legend>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">角色代号 <small>(英文/数字,用于 config.yaml)</small></span>
|
||||||
|
<input class="field__input" name="agentId" type="text" placeholder="eternal_lover_001" required pattern="[a-zA-Z0-9_]+" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">显示名称</span>
|
||||||
|
<input class="field__input" name="displayName" type="text" placeholder=" ta 的名字" required />
|
||||||
|
</label>
|
||||||
|
<label class="field field--half">
|
||||||
|
<span class="field__label">性别</span>
|
||||||
|
<select class="field__input" name="gender">
|
||||||
|
<option value="unknown">未知</option>
|
||||||
|
<option value="female">女</option>
|
||||||
|
<option value="male">男</option>
|
||||||
|
<option value="nonbinary">非二元</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field field--half">
|
||||||
|
<span class="field__label">年龄</span>
|
||||||
|
<input class="field__input" name="age" type="text" placeholder="24" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary" type="button" data-action="next">下一步</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Step 2: Soul -->
|
||||||
|
<fieldset class="form-step" data-step="1">
|
||||||
|
<legend class="form-step__legend">灵魂设定(Soul.md)</legend>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">背景故事</span>
|
||||||
|
<textarea class="field__input field__input--tall" name="background" placeholder="你们是如何相遇的?ta 经历过什么?" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">性格标签 <small>(用逗号分隔)</small></span>
|
||||||
|
<input class="field__input" name="personality" type="text" placeholder="温柔, 敏感, 嘴硬心软, 浪漫主义" required />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">说话风格</span>
|
||||||
|
<textarea class="field__input" name="speechStyle" placeholder="常用语气词、句式、称呼方式、口头禅……" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">喜好</span>
|
||||||
|
<textarea class="field__input" name="likes" placeholder="喜欢的事物、活动、话题"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">厌恶 / 底线</span>
|
||||||
|
<textarea class="field__input" name="dislikes" placeholder="讨厌什么,哪些话题会触发情绪波动"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions form-actions--split">
|
||||||
|
<button class="btn btn--ghost" type="button" data-action="prev">上一步</button>
|
||||||
|
<button class="btn btn--primary" type="button" data-action="next">下一步</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Step 3: Relationships & Memory -->
|
||||||
|
<fieldset class="form-step" data-step="2">
|
||||||
|
<legend class="form-step__legend">关系与记忆</legend>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">与使用者的关系</span>
|
||||||
|
<input class="field__input" name="relationship" type="text" placeholder="前任、挚友、恋人、家人……" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">共同记忆 / 关键事件</span>
|
||||||
|
<textarea class="field__input field__input--tall" name="memories" placeholder="那些只属于你们的高光或遗憾时刻"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">秘密或未说出口的话</span>
|
||||||
|
<textarea class="field__input" name="secrets" placeholder="藏在角色心底、会间接影响语气的细节"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">开场白</span>
|
||||||
|
<textarea class="field__input" name="greeting" placeholder="ta 第一次开口会对你说什么?" required></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions form-actions--split">
|
||||||
|
<button class="btn btn--ghost" type="button" data-action="prev">上一步</button>
|
||||||
|
<button class="btn btn--primary" type="button" data-action="next">下一步</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Step 4: Config -->
|
||||||
|
<fieldset class="form-step" data-step="3">
|
||||||
|
<legend class="form-step__legend">运行配置(config.yaml)</legend>
|
||||||
|
<div class="field-group">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">模型</span>
|
||||||
|
<input class="field__input" name="model" type="text" value="hermes-3-llama-3.1-70b" />
|
||||||
|
</label>
|
||||||
|
<label class="field field--half">
|
||||||
|
<span class="field__label">Temperature</span>
|
||||||
|
<input class="field__input" name="temperature" type="number" step="0.1" min="0" max="2" value="0.85" />
|
||||||
|
</label>
|
||||||
|
<label class="field field--half">
|
||||||
|
<span class="field__label">Max Tokens</span>
|
||||||
|
<input class="field__input" name="maxTokens" type="number" min="1" value="1024" />
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">系统提示词 <small>(可自动生成,也可手动覆盖)</small></span>
|
||||||
|
<textarea class="field__input field__input--tall" name="systemPrompt" id="system-prompt" placeholder="留空将根据 Soul.md 自动生成"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field field--checkbox">
|
||||||
|
<input type="checkbox" name="enableMemory" checked />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<span class="field__label">启用长期记忆</span>
|
||||||
|
</label>
|
||||||
|
<label class="field field--checkbox">
|
||||||
|
<input type="checkbox" name="enableTools" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<span class="field__label">启用外部工具</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions form-actions--split">
|
||||||
|
<button class="btn btn--ghost" type="button" data-action="prev">上一步</button>
|
||||||
|
<button class="btn btn--primary" type="button" data-action="publish">生成并发布</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Result Panel -->
|
||||||
|
<div id="result-panel" class="result-panel" hidden>
|
||||||
|
<div class="result-panel__header">
|
||||||
|
<h3 class="result-panel__title">角色已蒸馏完成</h3>
|
||||||
|
<p class="result-panel__desc">下载 Hermes agent 配置文件,或复制到项目中使用。</p>
|
||||||
|
</div>
|
||||||
|
<div class="result-panel__files">
|
||||||
|
<div class="file-card">
|
||||||
|
<div class="file-card__icon">📜</div>
|
||||||
|
<div class="file-card__info">
|
||||||
|
<strong>Soul.md</strong>
|
||||||
|
<small>角色灵魂文档</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--small btn--outline" type="button" data-download="soul">下载</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-card">
|
||||||
|
<div class="file-card__icon">⚙️</div>
|
||||||
|
<div class="file-card__info">
|
||||||
|
<strong>config.yaml</strong>
|
||||||
|
<small>运行配置</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--small btn--outline" type="button" data-download="config">下载</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-panel__preview">
|
||||||
|
<div class="preview-tabs" role="tablist" aria-label="配置文件预览">
|
||||||
|
<button class="preview-tab active" type="button" role="tab" aria-selected="true" data-tab="soul">Soul.md</button>
|
||||||
|
<button class="preview-tab" type="button" role="tab" aria-selected="false" data-tab="config">config.yaml</button>
|
||||||
|
</div>
|
||||||
|
<pre class="preview-code" id="preview-code"><code></code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--ghost" type="button" data-action="reset">再创建一个</button>
|
||||||
|
<button class="btn btn--primary" type="button" data-action="back-to-center">返回管理中心</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Tab Bar (U8) -->
|
||||||
|
<nav class="tab-bar" id="tab-bar" role="tablist" aria-label="主导航">
|
||||||
|
<button class="tab-bar__item active" type="button" role="tab" aria-selected="true" data-tab-action="tab-home">
|
||||||
|
<span class="tab-bar__icon" aria-hidden="true">⌂</span>
|
||||||
|
<span class="tab-bar__label">首页</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-bar__item" type="button" role="tab" aria-selected="false" data-tab-action="tab-distill">
|
||||||
|
<span class="tab-bar__icon" aria-hidden="true">♡</span>
|
||||||
|
<span class="tab-bar__label">蒸馏前任</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-bar__item" type="button" role="tab" aria-selected="false" data-tab-action="tab-mine">
|
||||||
|
<span class="tab-bar__icon" aria-hidden="true">☺</span>
|
||||||
|
<span class="tab-bar__label" id="tab-mine-label">我的</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Screen reader live region for view change announcements -->
|
||||||
|
<div id="sr-announce" class="visually-hidden" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,849 @@
|
||||||
|
{
|
||||||
|
"name": "eternalai",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^2.0.0",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"iconv-lite": "^0.7.2",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.15.2",
|
||||||
|
"raw-body": "^3.0.2",
|
||||||
|
"type-is": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.1",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.15.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
|
||||||
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.7.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^2.0.0",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
2.0.0 / 2024-08-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Drop node <18 support
|
||||||
|
* deps: mime-types@^3.0.0
|
||||||
|
* deps: negotiator@^1.0.0
|
||||||
|
|
||||||
|
1.3.8 / 2022-02-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.34
|
||||||
|
- deps: mime-db@~1.51.0
|
||||||
|
* deps: negotiator@0.6.3
|
||||||
|
|
||||||
|
1.3.7 / 2019-04-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.6.2
|
||||||
|
- Fix sorting charset, encoding, and language with extra parameters
|
||||||
|
|
||||||
|
1.3.6 / 2019-04-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.24
|
||||||
|
- deps: mime-db@~1.40.0
|
||||||
|
|
||||||
|
1.3.5 / 2018-02-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.18
|
||||||
|
- deps: mime-db@~1.33.0
|
||||||
|
|
||||||
|
1.3.4 / 2017-08-22
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.16
|
||||||
|
- deps: mime-db@~1.29.0
|
||||||
|
|
||||||
|
1.3.3 / 2016-05-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.11
|
||||||
|
- deps: mime-db@~1.23.0
|
||||||
|
* deps: negotiator@0.6.1
|
||||||
|
- perf: improve `Accept` parsing speed
|
||||||
|
- perf: improve `Accept-Charset` parsing speed
|
||||||
|
- perf: improve `Accept-Encoding` parsing speed
|
||||||
|
- perf: improve `Accept-Language` parsing speed
|
||||||
|
|
||||||
|
1.3.2 / 2016-03-08
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.10
|
||||||
|
- Fix extension of `application/dash+xml`
|
||||||
|
- Update primary extension for `audio/mp4`
|
||||||
|
- deps: mime-db@~1.22.0
|
||||||
|
|
||||||
|
1.3.1 / 2016-01-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.9
|
||||||
|
- deps: mime-db@~1.21.0
|
||||||
|
|
||||||
|
1.3.0 / 2015-09-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.7
|
||||||
|
- deps: mime-db@~1.19.0
|
||||||
|
* deps: negotiator@0.6.0
|
||||||
|
- Fix including type extensions in parameters in `Accept` parsing
|
||||||
|
- Fix parsing `Accept` parameters with quoted equals
|
||||||
|
- Fix parsing `Accept` parameters with quoted semicolons
|
||||||
|
- Lazy-load modules from main entry point
|
||||||
|
- perf: delay type concatenation until needed
|
||||||
|
- perf: enable strict mode
|
||||||
|
- perf: hoist regular expressions
|
||||||
|
- perf: remove closures getting spec properties
|
||||||
|
- perf: remove a closure from media type parsing
|
||||||
|
- perf: remove property delete from media type parsing
|
||||||
|
|
||||||
|
1.2.13 / 2015-09-06
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.6
|
||||||
|
- deps: mime-db@~1.18.0
|
||||||
|
|
||||||
|
1.2.12 / 2015-07-30
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.4
|
||||||
|
- deps: mime-db@~1.16.0
|
||||||
|
|
||||||
|
1.2.11 / 2015-07-16
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.3
|
||||||
|
- deps: mime-db@~1.15.0
|
||||||
|
|
||||||
|
1.2.10 / 2015-07-01
|
||||||
|
===================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.2
|
||||||
|
- deps: mime-db@~1.14.0
|
||||||
|
|
||||||
|
1.2.9 / 2015-06-08
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.1
|
||||||
|
- perf: fix deopt during mapping
|
||||||
|
|
||||||
|
1.2.8 / 2015-06-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.1.0
|
||||||
|
- deps: mime-db@~1.13.0
|
||||||
|
* perf: avoid argument reassignment & argument slice
|
||||||
|
* perf: avoid negotiator recursive construction
|
||||||
|
* perf: enable strict mode
|
||||||
|
* perf: remove unnecessary bitwise operator
|
||||||
|
|
||||||
|
1.2.7 / 2015-05-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.5.3
|
||||||
|
- Fix media type parameter matching to be case-insensitive
|
||||||
|
|
||||||
|
1.2.6 / 2015-05-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.11
|
||||||
|
- deps: mime-db@~1.9.1
|
||||||
|
* deps: negotiator@0.5.2
|
||||||
|
- Fix comparing media types with quoted values
|
||||||
|
- Fix splitting media types with quoted commas
|
||||||
|
|
||||||
|
1.2.5 / 2015-03-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.10
|
||||||
|
- deps: mime-db@~1.8.0
|
||||||
|
|
||||||
|
1.2.4 / 2015-02-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Support Node.js 0.6
|
||||||
|
* deps: mime-types@~2.0.9
|
||||||
|
- deps: mime-db@~1.7.0
|
||||||
|
* deps: negotiator@0.5.1
|
||||||
|
- Fix preference sorting to be stable for long acceptable lists
|
||||||
|
|
||||||
|
1.2.3 / 2015-01-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.8
|
||||||
|
- deps: mime-db@~1.6.0
|
||||||
|
|
||||||
|
1.2.2 / 2014-12-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.7
|
||||||
|
- deps: mime-db@~1.5.0
|
||||||
|
|
||||||
|
1.2.1 / 2014-12-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.5
|
||||||
|
- deps: mime-db@~1.3.1
|
||||||
|
|
||||||
|
1.2.0 / 2014-12-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.5.0
|
||||||
|
- Fix list return order when large accepted list
|
||||||
|
- Fix missing identity encoding when q=0 exists
|
||||||
|
- Remove dynamic building of Negotiator class
|
||||||
|
|
||||||
|
1.1.4 / 2014-12-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.4
|
||||||
|
- deps: mime-db@~1.3.0
|
||||||
|
|
||||||
|
1.1.3 / 2014-11-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.3
|
||||||
|
- deps: mime-db@~1.2.0
|
||||||
|
|
||||||
|
1.1.2 / 2014-10-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.9
|
||||||
|
- Fix error when media type has invalid parameter
|
||||||
|
|
||||||
|
1.1.1 / 2014-09-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: mime-types@~2.0.2
|
||||||
|
- deps: mime-db@~1.1.0
|
||||||
|
* deps: negotiator@0.4.8
|
||||||
|
- Fix all negotiations to be case-insensitive
|
||||||
|
- Stable sort preferences of same quality according to client order
|
||||||
|
|
||||||
|
1.1.0 / 2014-09-02
|
||||||
|
==================
|
||||||
|
|
||||||
|
* update `mime-types`
|
||||||
|
|
||||||
|
1.0.7 / 2014-07-04
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix wrong type returned from `type` when match after unknown extension
|
||||||
|
|
||||||
|
1.0.6 / 2014-06-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.7
|
||||||
|
|
||||||
|
1.0.5 / 2014-06-20
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix crash when unknown extension given
|
||||||
|
|
||||||
|
1.0.4 / 2014-06-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* use `mime-types`
|
||||||
|
|
||||||
|
1.0.3 / 2014-06-11
|
||||||
|
==================
|
||||||
|
|
||||||
|
* deps: negotiator@0.4.6
|
||||||
|
- Order by specificity when quality is the same
|
||||||
|
|
||||||
|
1.0.2 / 2014-05-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix interpretation when header not in request
|
||||||
|
* deps: pin negotiator@0.4.5
|
||||||
|
|
||||||
|
1.0.1 / 2014-01-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Identity encoding isn't always acceptable
|
||||||
|
* deps: negotiator@~0.4.0
|
||||||
|
|
||||||
|
1.0.0 / 2013-12-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Genesis
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||||
|
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||||
|
|
||||||
|
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,140 @@
|
||||||
|
# accepts
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Node.js Version][node-version-image]][node-version-url]
|
||||||
|
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||||
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator).
|
||||||
|
Extracted from [koa](https://www.npmjs.com/package/koa) for general use.
|
||||||
|
|
||||||
|
In addition to negotiator, it allows:
|
||||||
|
|
||||||
|
- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])`
|
||||||
|
as well as `('text/html', 'application/json')`.
|
||||||
|
- Allows type shorthands such as `json`.
|
||||||
|
- Returns `false` when no types match
|
||||||
|
- Treats non-existent headers as `*`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||||
|
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||||
|
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install accepts
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
var accepts = require('accepts')
|
||||||
|
```
|
||||||
|
|
||||||
|
### accepts(req)
|
||||||
|
|
||||||
|
Create a new `Accepts` object for the given `req`.
|
||||||
|
|
||||||
|
#### .charset(charsets)
|
||||||
|
|
||||||
|
Return the first accepted charset. If nothing in `charsets` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .charsets()
|
||||||
|
|
||||||
|
Return the charsets that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .encoding(encodings)
|
||||||
|
|
||||||
|
Return the first accepted encoding. If nothing in `encodings` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .encodings()
|
||||||
|
|
||||||
|
Return the encodings that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .language(languages)
|
||||||
|
|
||||||
|
Return the first accepted language. If nothing in `languages` is accepted,
|
||||||
|
then `false` is returned.
|
||||||
|
|
||||||
|
#### .languages()
|
||||||
|
|
||||||
|
Return the languages that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
#### .type(types)
|
||||||
|
|
||||||
|
Return the first accepted type (and it is returned as the same text as what
|
||||||
|
appears in the `types` array). If nothing in `types` is accepted, then `false`
|
||||||
|
is returned.
|
||||||
|
|
||||||
|
The `types` array can contain full MIME types or file extensions. Any value
|
||||||
|
that is not a full MIME type is passed to `require('mime-types').lookup`.
|
||||||
|
|
||||||
|
#### .types()
|
||||||
|
|
||||||
|
Return the types that the request accepts, in the order of the client's
|
||||||
|
preference (most preferred first).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Simple type negotiation
|
||||||
|
|
||||||
|
This simple example shows how to use `accepts` to return a different typed
|
||||||
|
respond body based on what the client wants to accept. The server lists it's
|
||||||
|
preferences in order and will get back the best match between the client and
|
||||||
|
server.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var accepts = require('accepts')
|
||||||
|
var http = require('http')
|
||||||
|
|
||||||
|
function app (req, res) {
|
||||||
|
var accept = accepts(req)
|
||||||
|
|
||||||
|
// the order of this list is significant; should be server preferred order
|
||||||
|
switch (accept.type(['json', 'html'])) {
|
||||||
|
case 'json':
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.write('{"hello":"world!"}')
|
||||||
|
break
|
||||||
|
case 'html':
|
||||||
|
res.setHeader('Content-Type', 'text/html')
|
||||||
|
res.write('<b>hello, world!</b>')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// the fallback is text/plain, so no need to specify it above
|
||||||
|
res.setHeader('Content-Type', 'text/plain')
|
||||||
|
res.write('hello, world!')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
http.createServer(app).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can test this out with the cURL program:
|
||||||
|
```sh
|
||||||
|
curl -I -H'Accept: text/html' http://localhost:3000/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master
|
||||||
|
[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci
|
||||||
|
[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml
|
||||||
|
[node-version-image]: https://badgen.net/npm/node/accepts
|
||||||
|
[node-version-url]: https://nodejs.org/en/download
|
||||||
|
[npm-downloads-image]: https://badgen.net/npm/dm/accepts
|
||||||
|
[npm-url]: https://npmjs.org/package/accepts
|
||||||
|
[npm-version-image]: https://badgen.net/npm/v/accepts
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
/*!
|
||||||
|
* accepts
|
||||||
|
* Copyright(c) 2014 Jonathan Ong
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Negotiator = require('negotiator')
|
||||||
|
var mime = require('mime-types')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = Accepts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Accepts object for the given req.
|
||||||
|
*
|
||||||
|
* @param {object} req
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Accepts (req) {
|
||||||
|
if (!(this instanceof Accepts)) {
|
||||||
|
return new Accepts(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headers = req.headers
|
||||||
|
this.negotiator = new Negotiator(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given `type(s)` is acceptable, returning
|
||||||
|
* the best match when true, otherwise `undefined`, in which
|
||||||
|
* case you should respond with 406 "Not Acceptable".
|
||||||
|
*
|
||||||
|
* The `type` value may be a single mime type string
|
||||||
|
* such as "application/json", the extension name
|
||||||
|
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
|
||||||
|
* or array is given the _best_ match, if any is returned.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* // Accept: text/html
|
||||||
|
* this.types('html');
|
||||||
|
* // => "html"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.types('html');
|
||||||
|
* // => "html"
|
||||||
|
* this.types('text/html');
|
||||||
|
* // => "text/html"
|
||||||
|
* this.types('json', 'text');
|
||||||
|
* // => "json"
|
||||||
|
* this.types('application/json');
|
||||||
|
* // => "application/json"
|
||||||
|
*
|
||||||
|
* // Accept: text/*, application/json
|
||||||
|
* this.types('image/png');
|
||||||
|
* this.types('png');
|
||||||
|
* // => undefined
|
||||||
|
*
|
||||||
|
* // Accept: text/*;q=.5, application/json
|
||||||
|
* this.types(['html', 'json']);
|
||||||
|
* this.types('html', 'json');
|
||||||
|
* // => "json"
|
||||||
|
*
|
||||||
|
* @param {String|Array} types...
|
||||||
|
* @return {String|Array|Boolean}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.type =
|
||||||
|
Accepts.prototype.types = function (types_) {
|
||||||
|
var types = types_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (types && !Array.isArray(types)) {
|
||||||
|
types = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < types.length; i++) {
|
||||||
|
types[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no types, return all requested types
|
||||||
|
if (!types || types.length === 0) {
|
||||||
|
return this.negotiator.mediaTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no accept header, return first given type
|
||||||
|
if (!this.headers.accept) {
|
||||||
|
return types[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimes = types.map(extToMime)
|
||||||
|
var accepts = this.negotiator.mediaTypes(mimes.filter(validMime))
|
||||||
|
var first = accepts[0]
|
||||||
|
|
||||||
|
return first
|
||||||
|
? types[mimes.indexOf(first)]
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted encodings or best fit based on `encodings`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Encoding: gzip, deflate`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['gzip', 'deflate']
|
||||||
|
*
|
||||||
|
* @param {String|Array} encodings...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.encoding =
|
||||||
|
Accepts.prototype.encodings = function (encodings_) {
|
||||||
|
var encodings = encodings_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (encodings && !Array.isArray(encodings)) {
|
||||||
|
encodings = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < encodings.length; i++) {
|
||||||
|
encodings[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no encodings, return all requested encodings
|
||||||
|
if (!encodings || encodings.length === 0) {
|
||||||
|
return this.negotiator.encodings()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.encodings(encodings)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted charsets or best fit based on `charsets`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['utf-8', 'utf-7', 'iso-8859-1']
|
||||||
|
*
|
||||||
|
* @param {String|Array} charsets...
|
||||||
|
* @return {String|Array}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.charset =
|
||||||
|
Accepts.prototype.charsets = function (charsets_) {
|
||||||
|
var charsets = charsets_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (charsets && !Array.isArray(charsets)) {
|
||||||
|
charsets = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < charsets.length; i++) {
|
||||||
|
charsets[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no charsets, return all requested charsets
|
||||||
|
if (!charsets || charsets.length === 0) {
|
||||||
|
return this.negotiator.charsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.charsets(charsets)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return accepted languages or best fit based on `langs`.
|
||||||
|
*
|
||||||
|
* Given `Accept-Language: en;q=0.8, es, pt`
|
||||||
|
* an array sorted by quality is returned:
|
||||||
|
*
|
||||||
|
* ['es', 'pt', 'en']
|
||||||
|
*
|
||||||
|
* @param {String|Array} langs...
|
||||||
|
* @return {Array|String}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
Accepts.prototype.lang =
|
||||||
|
Accepts.prototype.langs =
|
||||||
|
Accepts.prototype.language =
|
||||||
|
Accepts.prototype.languages = function (languages_) {
|
||||||
|
var languages = languages_
|
||||||
|
|
||||||
|
// support flattened arguments
|
||||||
|
if (languages && !Array.isArray(languages)) {
|
||||||
|
languages = new Array(arguments.length)
|
||||||
|
for (var i = 0; i < languages.length; i++) {
|
||||||
|
languages[i] = arguments[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no languages, return all requested languages
|
||||||
|
if (!languages || languages.length === 0) {
|
||||||
|
return this.negotiator.languages()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.negotiator.languages(languages)[0] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert extnames to mime.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {String}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function extToMime (type) {
|
||||||
|
return type.indexOf('/') === -1
|
||||||
|
? mime.lookup(type)
|
||||||
|
: type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if mime is valid.
|
||||||
|
*
|
||||||
|
* @param {String} type
|
||||||
|
* @return {Boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function validMime (type) {
|
||||||
|
return typeof type === 'string'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "accepts",
|
||||||
|
"description": "Higher-level content negotiation",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"contributors": [
|
||||||
|
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "jshttp/accepts",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"deep-equal": "1.0.1",
|
||||||
|
"eslint": "7.32.0",
|
||||||
|
"eslint-config-standard": "14.1.1",
|
||||||
|
"eslint-plugin-import": "2.25.4",
|
||||||
|
"eslint-plugin-markdown": "2.2.1",
|
||||||
|
"eslint-plugin-node": "11.1.0",
|
||||||
|
"eslint-plugin-promise": "4.3.1",
|
||||||
|
"eslint-plugin-standard": "4.1.0",
|
||||||
|
"mocha": "9.2.0",
|
||||||
|
"nyc": "15.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"LICENSE",
|
||||||
|
"HISTORY.md",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||||
|
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"content",
|
||||||
|
"negotiation",
|
||||||
|
"accept",
|
||||||
|
"accepts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||||
|
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||||
|
|
||||||
|
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,509 @@
|
||||||
|
# body-parser
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Build Status][ci-image]][ci-url]
|
||||||
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||||
|
[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer]
|
||||||
|
|
||||||
|
Node.js body parsing middleware.
|
||||||
|
|
||||||
|
Parse incoming request bodies in a middleware before your handlers, available
|
||||||
|
under the `req.body` property.
|
||||||
|
|
||||||
|
**Note** As `req.body`'s shape is based on user-controlled input, all
|
||||||
|
properties and values in this object are untrusted and should be validated
|
||||||
|
before trusting. For example, `req.body.foo.toString()` may fail in multiple
|
||||||
|
ways, for example the `foo` property may not be there or may not be a string,
|
||||||
|
and `toString` may not be a function and instead a string or other user input.
|
||||||
|
|
||||||
|
[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/learn/http/anatomy-of-an-http-transaction).
|
||||||
|
|
||||||
|
_This does not handle multipart bodies_, due to their complex and typically
|
||||||
|
large nature. For multipart bodies, you may be interested in the following
|
||||||
|
modules:
|
||||||
|
|
||||||
|
* [busboy](https://www.npmjs.com/package/busboy#readme) and
|
||||||
|
[connect-busboy](https://www.npmjs.com/package/connect-busboy#readme)
|
||||||
|
* [multiparty](https://www.npmjs.com/package/multiparty#readme) and
|
||||||
|
[connect-multiparty](https://www.npmjs.com/package/connect-multiparty#readme)
|
||||||
|
* [formidable](https://www.npmjs.com/package/formidable#readme)
|
||||||
|
* [multer](https://www.npmjs.com/package/multer#readme)
|
||||||
|
|
||||||
|
This module provides the following parsers:
|
||||||
|
|
||||||
|
* [JSON body parser](#bodyparserjsonoptions)
|
||||||
|
* [Raw body parser](#bodyparserrawoptions)
|
||||||
|
* [Text body parser](#bodyparsertextoptions)
|
||||||
|
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
|
||||||
|
|
||||||
|
Other body parsers you might be interested in:
|
||||||
|
|
||||||
|
- [body](https://www.npmjs.com/package/body#readme)
|
||||||
|
- [co-body](https://www.npmjs.com/package/co-body#readme)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install body-parser
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Import all parsers
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
|
||||||
|
// Or import individual parsers directly
|
||||||
|
const json = require('body-parser/json')
|
||||||
|
const urlencoded = require('body-parser/urlencoded')
|
||||||
|
const raw = require('body-parser/raw')
|
||||||
|
const text = require('body-parser/text')
|
||||||
|
```
|
||||||
|
|
||||||
|
The `bodyParser` object exposes various factories to create middlewares. All
|
||||||
|
middlewares will populate the `req.body` property with the parsed body when
|
||||||
|
the `Content-Type` request header matches the `type` option.
|
||||||
|
|
||||||
|
The various errors returned by this module are described in the
|
||||||
|
[errors section](#errors).
|
||||||
|
|
||||||
|
### bodyParser.json([options])
|
||||||
|
|
||||||
|
Returns middleware that only parses `json` and only looks at requests where
|
||||||
|
the `Content-Type` header matches the `type` option. This parser accepts any
|
||||||
|
Unicode encoding of the body and supports automatic inflation of `gzip`,
|
||||||
|
`br` (brotli) and `deflate` encodings.
|
||||||
|
|
||||||
|
A new `body` object containing the parsed data is populated on the `request`
|
||||||
|
object after the middleware (i.e. `req.body`).
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
The `json` function takes an optional `options` object that may contain any of
|
||||||
|
the following keys:
|
||||||
|
|
||||||
|
##### defaultCharset
|
||||||
|
|
||||||
|
Specify the default character set for the json content if the charset is not
|
||||||
|
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
||||||
|
|
||||||
|
##### inflate
|
||||||
|
|
||||||
|
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||||
|
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||||
|
|
||||||
|
##### limit
|
||||||
|
|
||||||
|
Controls the maximum request body size. If this is a number, then the value
|
||||||
|
specifies the number of bytes; if it is a string, the value is passed to the
|
||||||
|
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||||
|
to `'100kb'`.
|
||||||
|
|
||||||
|
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||||
|
|
||||||
|
##### reviver
|
||||||
|
|
||||||
|
The `reviver` option is passed directly to `JSON.parse` as the second
|
||||||
|
argument. You can find more information on this argument
|
||||||
|
[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#the_reviver_parameter).
|
||||||
|
|
||||||
|
##### strict
|
||||||
|
|
||||||
|
When set to `true`, will only accept arrays and objects; when `false` will
|
||||||
|
accept anything `JSON.parse` accepts. Defaults to `true`.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
The `type` option is used to determine what media type the middleware will
|
||||||
|
parse. This option can be a string, array of strings, or a function. If not a
|
||||||
|
function, `type` option is passed directly to the
|
||||||
|
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||||
|
be an extension name (like `json`), a mime type (like `application/json`), or
|
||||||
|
a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type`
|
||||||
|
option is called as `fn(req)` and the request is parsed if it returns a truthy
|
||||||
|
value. Defaults to `application/json`.
|
||||||
|
|
||||||
|
##### verify
|
||||||
|
|
||||||
|
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||||
|
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||||
|
encoding of the request. The parsing can be aborted by throwing an error.
|
||||||
|
|
||||||
|
### bodyParser.raw([options])
|
||||||
|
|
||||||
|
Returns middleware that parses all bodies as a `Buffer` and only looks at
|
||||||
|
requests where the `Content-Type` header matches the `type` option. This
|
||||||
|
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||||
|
encodings.
|
||||||
|
|
||||||
|
A new `body` object containing the parsed data is populated on the `request`
|
||||||
|
object after the middleware (i.e. `req.body`). This will be a `Buffer` object
|
||||||
|
of the body.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
The `raw` function takes an optional `options` object that may contain any of
|
||||||
|
the following keys:
|
||||||
|
|
||||||
|
##### inflate
|
||||||
|
|
||||||
|
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||||
|
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||||
|
|
||||||
|
##### limit
|
||||||
|
|
||||||
|
Controls the maximum request body size. If this is a number, then the value
|
||||||
|
specifies the number of bytes; if it is a string, the value is passed to the
|
||||||
|
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||||
|
to `'100kb'`.
|
||||||
|
|
||||||
|
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
The `type` option is used to determine what media type the middleware will
|
||||||
|
parse. This option can be a string, array of strings, or a function.
|
||||||
|
If not a function, `type` option is passed directly to the
|
||||||
|
[type-is](https://www.npmjs.com/package/type-is#readme) library and this
|
||||||
|
can be an extension name (like `bin`), a mime type (like
|
||||||
|
`application/octet-stream`), or a mime type with a wildcard (like `*/*` or
|
||||||
|
`application/*`). If a function, the `type` option is called as `fn(req)`
|
||||||
|
and the request is parsed if it returns a truthy value. Defaults to
|
||||||
|
`application/octet-stream`.
|
||||||
|
|
||||||
|
##### verify
|
||||||
|
|
||||||
|
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||||
|
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||||
|
encoding of the request. The parsing can be aborted by throwing an error.
|
||||||
|
|
||||||
|
### bodyParser.text([options])
|
||||||
|
|
||||||
|
Returns middleware that parses all bodies as a string and only looks at
|
||||||
|
requests where the `Content-Type` header matches the `type` option. This
|
||||||
|
parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate`
|
||||||
|
encodings.
|
||||||
|
|
||||||
|
A new `body` string containing the parsed data is populated on the `request`
|
||||||
|
object after the middleware (i.e. `req.body`). This will be a string of the
|
||||||
|
body.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
The `text` function takes an optional `options` object that may contain any of
|
||||||
|
the following keys:
|
||||||
|
|
||||||
|
##### defaultCharset
|
||||||
|
|
||||||
|
Specify the default character set for the text content if the charset is not
|
||||||
|
specified in the `Content-Type` header of the request. Defaults to `utf-8`.
|
||||||
|
|
||||||
|
##### inflate
|
||||||
|
|
||||||
|
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||||
|
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||||
|
|
||||||
|
##### limit
|
||||||
|
|
||||||
|
Controls the maximum request body size. If this is a number, then the value
|
||||||
|
specifies the number of bytes; if it is a string, the value is passed to the
|
||||||
|
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||||
|
to `'100kb'`.
|
||||||
|
|
||||||
|
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
The `type` option is used to determine what media type the middleware will
|
||||||
|
parse. This option can be a string, array of strings, or a function. If not
|
||||||
|
a function, `type` option is passed directly to the
|
||||||
|
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||||
|
be an extension name (like `txt`), a mime type (like `text/plain`), or a mime
|
||||||
|
type with a wildcard (like `*/*` or `text/*`). If a function, the `type`
|
||||||
|
option is called as `fn(req)` and the request is parsed if it returns a
|
||||||
|
truthy value. Defaults to `text/plain`.
|
||||||
|
|
||||||
|
##### verify
|
||||||
|
|
||||||
|
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||||
|
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||||
|
encoding of the request. The parsing can be aborted by throwing an error.
|
||||||
|
|
||||||
|
### bodyParser.urlencoded([options])
|
||||||
|
|
||||||
|
Returns middleware that only parses `urlencoded` bodies and only looks at
|
||||||
|
requests where the `Content-Type` header matches the `type` option. This
|
||||||
|
parser accepts only UTF-8 and ISO-8859-1 encodings of the body and supports
|
||||||
|
automatic inflation of `gzip`, `br` (brotli) and `deflate` encodings.
|
||||||
|
|
||||||
|
A new `body` object containing the parsed data is populated on the `request`
|
||||||
|
object after the middleware (i.e. `req.body`). This object will contain
|
||||||
|
key-value pairs, where the value can be a string or array (when `extended` is
|
||||||
|
`false`), or any type (when `extended` is `true`).
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
The `urlencoded` function takes an optional `options` object that may contain
|
||||||
|
any of the following keys:
|
||||||
|
|
||||||
|
##### extended
|
||||||
|
|
||||||
|
The "extended" syntax allows for rich objects and arrays to be encoded into the
|
||||||
|
URL-encoded format, allowing for a JSON-like experience with URL-encoded. For
|
||||||
|
more information, please [see the qs
|
||||||
|
library](https://www.npmjs.com/package/qs#readme).
|
||||||
|
|
||||||
|
Defaults to `false`.
|
||||||
|
|
||||||
|
##### inflate
|
||||||
|
|
||||||
|
When set to `true`, then deflated (compressed) bodies will be inflated; when
|
||||||
|
`false`, deflated bodies are rejected. Defaults to `true`.
|
||||||
|
|
||||||
|
##### limit
|
||||||
|
|
||||||
|
Controls the maximum request body size. If this is a number, then the value
|
||||||
|
specifies the number of bytes; if it is a string, the value is passed to the
|
||||||
|
[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults
|
||||||
|
to `'100kb'`.
|
||||||
|
|
||||||
|
> It’s recommended not to configure a very high limit and to use the default value whenever possible. Allowing larger payloads increases memory usage because of the resources required for decoding and transformations, and it can also lead to longer response times as more data is processed. By ‘very high’, we mean values above the default, for example payloads of 5 MB or more can already start to introduce these risks. With the default limits, these issues do not occur.
|
||||||
|
|
||||||
|
##### parameterLimit
|
||||||
|
|
||||||
|
The `parameterLimit` option controls the maximum number of parameters that
|
||||||
|
are allowed in the URL-encoded data. If a request contains more parameters
|
||||||
|
than this value, a 413 will be returned to the client. Defaults to `1000`.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
The `type` option is used to determine what media type the middleware will
|
||||||
|
parse. This option can be a string, array of strings, or a function. If not
|
||||||
|
a function, `type` option is passed directly to the
|
||||||
|
[type-is](https://www.npmjs.com/package/type-is#readme) library and this can
|
||||||
|
be an extension name (like `urlencoded`), a mime type (like
|
||||||
|
`application/x-www-form-urlencoded`), or a mime type with a wildcard (like
|
||||||
|
`*/x-www-form-urlencoded`). If a function, the `type` option is called as
|
||||||
|
`fn(req)` and the request is parsed if it returns a truthy value. Defaults
|
||||||
|
to `application/x-www-form-urlencoded`.
|
||||||
|
|
||||||
|
##### verify
|
||||||
|
|
||||||
|
The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`,
|
||||||
|
where `buf` is a `Buffer` of the raw request body and `encoding` is the
|
||||||
|
encoding of the request. The parsing can be aborted by throwing an error.
|
||||||
|
|
||||||
|
##### defaultCharset
|
||||||
|
|
||||||
|
The default charset to parse as, if not specified in content-type. Must be
|
||||||
|
either `utf-8` or `iso-8859-1`. Defaults to `utf-8`.
|
||||||
|
|
||||||
|
##### charsetSentinel
|
||||||
|
|
||||||
|
Whether to let the value of the `utf8` parameter take precedence as the charset
|
||||||
|
selector. It requires the form to contain a parameter named `utf8` with a value
|
||||||
|
of `✓`. Defaults to `false`.
|
||||||
|
|
||||||
|
##### interpretNumericEntities
|
||||||
|
|
||||||
|
Whether to decode numeric entities such as `☺` when parsing an iso-8859-1
|
||||||
|
form. Defaults to `false`.
|
||||||
|
|
||||||
|
|
||||||
|
##### depth
|
||||||
|
|
||||||
|
The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
The middlewares provided by this module create errors using the
|
||||||
|
[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors
|
||||||
|
will typically have a `status`/`statusCode` property that contains the suggested
|
||||||
|
HTTP response code, an `expose` property to determine if the `message` property
|
||||||
|
should be displayed to the client, a `type` property to determine the type of
|
||||||
|
error without matching against the `message`, and a `body` property containing
|
||||||
|
the read body, if available.
|
||||||
|
|
||||||
|
The following are the common errors created, though any error can come through
|
||||||
|
for various reasons.
|
||||||
|
|
||||||
|
### content encoding unsupported
|
||||||
|
|
||||||
|
This error will occur when the request had a `Content-Encoding` header that
|
||||||
|
contained an encoding but the "inflation" option was set to `false`. The
|
||||||
|
`status` property is set to `415`, the `type` property is set to
|
||||||
|
`'encoding.unsupported'`, and the `charset` property will be set to the
|
||||||
|
encoding that is unsupported.
|
||||||
|
|
||||||
|
### entity parse failed
|
||||||
|
|
||||||
|
This error will occur when the request contained an entity that could not be
|
||||||
|
parsed by the middleware. The `status` property is set to `400`, the `type`
|
||||||
|
property is set to `'entity.parse.failed'`, and the `body` property is set to
|
||||||
|
the entity value that failed parsing.
|
||||||
|
|
||||||
|
### entity verify failed
|
||||||
|
|
||||||
|
This error will occur when the request contained an entity that could not be
|
||||||
|
failed verification by the defined `verify` option. The `status` property is
|
||||||
|
set to `403`, the `type` property is set to `'entity.verify.failed'`, and the
|
||||||
|
`body` property is set to the entity value that failed verification.
|
||||||
|
|
||||||
|
### request aborted
|
||||||
|
|
||||||
|
This error will occur when the request is aborted by the client before reading
|
||||||
|
the body has finished. The `received` property will be set to the number of
|
||||||
|
bytes received before the request was aborted and the `expected` property is
|
||||||
|
set to the number of expected bytes. The `status` property is set to `400`
|
||||||
|
and `type` property is set to `'request.aborted'`.
|
||||||
|
|
||||||
|
### request entity too large
|
||||||
|
|
||||||
|
This error will occur when the request body's size is larger than the "limit"
|
||||||
|
option. The `limit` property will be set to the byte limit and the `length`
|
||||||
|
property will be set to the request body's length. The `status` property is
|
||||||
|
set to `413` and the `type` property is set to `'entity.too.large'`.
|
||||||
|
|
||||||
|
### request size did not match content length
|
||||||
|
|
||||||
|
This error will occur when the request's length did not match the length from
|
||||||
|
the `Content-Length` header. This typically occurs when the request is malformed,
|
||||||
|
typically when the `Content-Length` header was calculated based on characters
|
||||||
|
instead of bytes. The `status` property is set to `400` and the `type` property
|
||||||
|
is set to `'request.size.invalid'`.
|
||||||
|
|
||||||
|
### stream encoding should not be set
|
||||||
|
|
||||||
|
This error will occur when something called the `req.setEncoding` method prior
|
||||||
|
to this middleware. This module operates directly on bytes only and you cannot
|
||||||
|
call `req.setEncoding` when using this module. The `status` property is set to
|
||||||
|
`500` and the `type` property is set to `'stream.encoding.set'`.
|
||||||
|
|
||||||
|
### stream is not readable
|
||||||
|
|
||||||
|
This error will occur when the request is no longer readable when this middleware
|
||||||
|
attempts to read it. This typically means something other than a middleware from
|
||||||
|
this module read the request body already and the middleware was also configured to
|
||||||
|
read the same request. The `status` property is set to `500` and the `type`
|
||||||
|
property is set to `'stream.not.readable'`.
|
||||||
|
|
||||||
|
### too many parameters
|
||||||
|
|
||||||
|
This error will occur when the content of the request exceeds the configured
|
||||||
|
`parameterLimit` for the `urlencoded` parser. The `status` property is set to
|
||||||
|
`413` and the `type` property is set to `'parameters.too.many'`.
|
||||||
|
|
||||||
|
### unsupported charset "BOGUS"
|
||||||
|
|
||||||
|
This error will occur when the request had a charset parameter in the
|
||||||
|
`Content-Type` header, but the `iconv-lite` module does not support it OR the
|
||||||
|
parser does not support it. The charset is contained in the message as well
|
||||||
|
as in the `charset` property. The `status` property is set to `415`, the
|
||||||
|
`type` property is set to `'charset.unsupported'`, and the `charset` property
|
||||||
|
is set to the charset that is unsupported.
|
||||||
|
|
||||||
|
### unsupported content encoding "bogus"
|
||||||
|
|
||||||
|
This error will occur when the request had a `Content-Encoding` header that
|
||||||
|
contained an unsupported encoding. The encoding is contained in the message
|
||||||
|
as well as in the `encoding` property. The `status` property is set to `415`,
|
||||||
|
the `type` property is set to `'encoding.unsupported'`, and the `encoding`
|
||||||
|
property is set to the encoding that is unsupported.
|
||||||
|
|
||||||
|
### The input exceeded the depth
|
||||||
|
|
||||||
|
This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Express/Connect top-level generic
|
||||||
|
|
||||||
|
This example demonstrates adding a generic JSON and URL-encoded parser as a
|
||||||
|
top-level middleware, which will parse the bodies of all incoming requests.
|
||||||
|
This is the simplest setup.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const express = require('express')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// parse application/x-www-form-urlencoded
|
||||||
|
app.use(bodyParser.urlencoded())
|
||||||
|
|
||||||
|
// parse application/json
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
|
app.use(function (req, res) {
|
||||||
|
res.setHeader('Content-Type', 'text/plain')
|
||||||
|
res.write('you posted:\n')
|
||||||
|
res.end(String(JSON.stringify(req.body, null, 2)))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Express route-specific
|
||||||
|
|
||||||
|
This example demonstrates adding body parsers specifically to the routes that
|
||||||
|
need them. In general, this is the most recommended way to use body-parser with
|
||||||
|
Express.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const express = require('express')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// create application/json parser
|
||||||
|
const jsonParser = bodyParser.json()
|
||||||
|
|
||||||
|
// create application/x-www-form-urlencoded parser
|
||||||
|
const urlencodedParser = bodyParser.urlencoded()
|
||||||
|
|
||||||
|
// POST /login gets urlencoded bodies
|
||||||
|
app.post('/login', urlencodedParser, function (req, res) {
|
||||||
|
if (!req.body || !req.body.username) res.sendStatus(400)
|
||||||
|
res.send('welcome, ' + req.body.username)
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /api/users gets JSON bodies
|
||||||
|
app.post('/api/users', jsonParser, function (req, res) {
|
||||||
|
if (!req.body) res.sendStatus(400)
|
||||||
|
// create user in req.body
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change accepted type for parsers
|
||||||
|
|
||||||
|
All the parsers accept a `type` option which allows you to change the
|
||||||
|
`Content-Type` that the middleware will parse.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const express = require('express')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// parse various different custom JSON types as JSON
|
||||||
|
app.use(bodyParser.json({ type: 'application/*+json' }))
|
||||||
|
|
||||||
|
// parse some custom thing into a Buffer
|
||||||
|
app.use(bodyParser.raw({ type: 'application/vnd.custom-type' }))
|
||||||
|
|
||||||
|
// parse an HTML body into a string
|
||||||
|
app.use(bodyParser.text({ type: 'text/html' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/body-parser/ci.yml?branch=master&label=ci
|
||||||
|
[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml
|
||||||
|
[coveralls-image]: https://img.shields.io/coverallsCoverage/github/expressjs/body-parser?branch=master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master
|
||||||
|
[npm-downloads-image]: https://img.shields.io/npm/dm/body-parser
|
||||||
|
[npm-url]: https://npmjs.com/package/body-parser
|
||||||
|
[npm-version-image]: https://img.shields.io/npm/v/body-parser
|
||||||
|
[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge
|
||||||
|
[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Parsers
|
||||||
|
* @property {Function} json JSON parser
|
||||||
|
* @property {Function} raw Raw parser
|
||||||
|
* @property {Function} text Text parser
|
||||||
|
* @property {Function} urlencoded URL-encoded parser
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @type {Function & Parsers}
|
||||||
|
*/
|
||||||
|
exports = module.exports = bodyParser
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON parser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
exports.json = require('./lib/types/json')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw parser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
exports.raw = require('./lib/types/raw')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text parser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
exports.text = require('./lib/types/text')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL-encoded parser.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
exports.urlencoded = require('./lib/types/urlencoded')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware to parse json and urlencoded bodies.
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function bodyParser () {
|
||||||
|
throw new Error('The bodyParser() generic has been split into individual middleware to use instead.')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createError = require('http-errors')
|
||||||
|
const getBody = require('raw-body')
|
||||||
|
const iconv = require('iconv-lite')
|
||||||
|
const onFinished = require('on-finished')
|
||||||
|
const zlib = require('node:zlib')
|
||||||
|
const hasBody = require('type-is').hasBody
|
||||||
|
const { getCharset } = require('./utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = read
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a request into a buffer and parse.
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Object} res
|
||||||
|
* @param {Function} next
|
||||||
|
* @param {Function} parse
|
||||||
|
* @param {Function} debug
|
||||||
|
* @param {Object} options
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function read (req, res, next, parse, debug, options) {
|
||||||
|
if (onFinished.isFinished(req)) {
|
||||||
|
debug('body already parsed')
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('body' in req)) {
|
||||||
|
req.body = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip requests without bodies
|
||||||
|
if (!hasBody(req)) {
|
||||||
|
debug('skip empty body')
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('content-type %j', req.headers['content-type'])
|
||||||
|
|
||||||
|
// determine if request should be parsed
|
||||||
|
if (!options.shouldParse(req)) {
|
||||||
|
debug('skip parsing')
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoding = null
|
||||||
|
if (options?.skipCharset !== true) {
|
||||||
|
encoding = getCharset(req) || options.defaultCharset
|
||||||
|
|
||||||
|
// validate charset
|
||||||
|
if (!!options?.isValidCharset && !options.isValidCharset(encoding)) {
|
||||||
|
debug('invalid charset')
|
||||||
|
next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||||
|
charset: encoding,
|
||||||
|
type: 'charset.unsupported'
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let length
|
||||||
|
const opts = options
|
||||||
|
let stream
|
||||||
|
|
||||||
|
// read options
|
||||||
|
const verify = opts.verify
|
||||||
|
|
||||||
|
try {
|
||||||
|
// get the content stream
|
||||||
|
stream = contentstream(req, debug, opts.inflate)
|
||||||
|
length = stream.length
|
||||||
|
stream.length = undefined
|
||||||
|
} catch (err) {
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set raw-body options
|
||||||
|
opts.length = length
|
||||||
|
opts.encoding = verify
|
||||||
|
? null
|
||||||
|
: encoding
|
||||||
|
|
||||||
|
// assert charset is supported
|
||||||
|
if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
|
||||||
|
return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||||
|
charset: encoding.toLowerCase(),
|
||||||
|
type: 'charset.unsupported'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// read body
|
||||||
|
debug('read body')
|
||||||
|
getBody(stream, opts, function (error, body) {
|
||||||
|
if (error) {
|
||||||
|
let _error
|
||||||
|
|
||||||
|
if (error.type === 'encoding.unsupported') {
|
||||||
|
// echo back charset
|
||||||
|
_error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
|
||||||
|
charset: encoding.toLowerCase(),
|
||||||
|
type: 'charset.unsupported'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// set status code on error
|
||||||
|
_error = createError(400, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpipe from stream and destroy
|
||||||
|
if (stream !== req) {
|
||||||
|
req.unpipe()
|
||||||
|
stream.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read off entire request
|
||||||
|
dump(req, function onfinished () {
|
||||||
|
next(createError(400, _error))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify
|
||||||
|
if (verify) {
|
||||||
|
try {
|
||||||
|
debug('verify body')
|
||||||
|
verify(req, res, body, encoding)
|
||||||
|
} catch (err) {
|
||||||
|
next(createError(403, err, {
|
||||||
|
body: body,
|
||||||
|
type: err.type || 'entity.verify.failed'
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse
|
||||||
|
let str = body
|
||||||
|
try {
|
||||||
|
debug('parse body')
|
||||||
|
str = typeof body !== 'string' && encoding !== null
|
||||||
|
? iconv.decode(body, encoding)
|
||||||
|
: body
|
||||||
|
req.body = parse(str, encoding)
|
||||||
|
} catch (err) {
|
||||||
|
next(createError(400, err, {
|
||||||
|
body: str,
|
||||||
|
type: err.type || 'entity.parse.failed'
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content stream of the request.
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Function} debug
|
||||||
|
* @param {boolean} inflate
|
||||||
|
* @returns {Object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function contentstream (req, debug, inflate) {
|
||||||
|
const encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
|
||||||
|
const length = req.headers['content-length']
|
||||||
|
|
||||||
|
debug('content-encoding "%s"', encoding)
|
||||||
|
|
||||||
|
if (inflate === false && encoding !== 'identity') {
|
||||||
|
throw createError(415, 'content encoding unsupported', {
|
||||||
|
encoding: encoding,
|
||||||
|
type: 'encoding.unsupported'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding === 'identity') {
|
||||||
|
req.length = length
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = createDecompressionStream(encoding, debug)
|
||||||
|
req.pipe(stream)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a decompression stream for the given encoding.
|
||||||
|
* @param {string} encoding
|
||||||
|
* @param {Function} debug
|
||||||
|
* @returns {Object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function createDecompressionStream (encoding, debug) {
|
||||||
|
switch (encoding) {
|
||||||
|
case 'deflate':
|
||||||
|
debug('inflate body')
|
||||||
|
return zlib.createInflate()
|
||||||
|
case 'gzip':
|
||||||
|
debug('gunzip body')
|
||||||
|
return zlib.createGunzip()
|
||||||
|
case 'br':
|
||||||
|
debug('brotli decompress body')
|
||||||
|
return zlib.createBrotliDecompress()
|
||||||
|
default:
|
||||||
|
throw createError(415, 'unsupported content encoding "' + encoding + '"', {
|
||||||
|
encoding: encoding,
|
||||||
|
type: 'encoding.unsupported'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump the contents of a request.
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Function} callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function dump (req, callback) {
|
||||||
|
if (onFinished.isFinished(req)) {
|
||||||
|
callback(null)
|
||||||
|
} else {
|
||||||
|
onFinished(req, callback)
|
||||||
|
req.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014 Jonathan Ong
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = require('debug')('body-parser:json')
|
||||||
|
const read = require('../read')
|
||||||
|
const { normalizeOptions } = require('../utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = json
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match the first non-space in a string.
|
||||||
|
*
|
||||||
|
* Allowed whitespace is defined in RFC 7159:
|
||||||
|
*
|
||||||
|
* ws = *(
|
||||||
|
* %x20 / ; Space
|
||||||
|
* %x09 / ; Horizontal tab
|
||||||
|
* %x0A / ; Line feed or New line
|
||||||
|
* %x0D ) ; Carriage return
|
||||||
|
*/
|
||||||
|
const FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
const JSON_SYNTAX_CHAR = '#'
|
||||||
|
const JSON_SYNTAX_REGEXP = /#+/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware to parse JSON bodies.
|
||||||
|
*
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @returns {Function}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function json (options) {
|
||||||
|
const normalizedOptions = normalizeOptions(options, 'application/json')
|
||||||
|
|
||||||
|
const parse = createJsonParser(options)
|
||||||
|
|
||||||
|
const readOptions = {
|
||||||
|
...normalizedOptions,
|
||||||
|
// assert charset per RFC 7159 sec 8.1
|
||||||
|
isValidCharset: (charset) => charset.slice(0, 4) === 'utf-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return function jsonParser (req, res, next) {
|
||||||
|
read(req, res, next, parse, debug, readOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSON parse function
|
||||||
|
*
|
||||||
|
* @param {object} [options]
|
||||||
|
* @return {function}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function createJsonParser (options) {
|
||||||
|
const reviver = options?.reviver
|
||||||
|
const strict = options?.strict !== false
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
return function parse (body) {
|
||||||
|
if (body.length === 0) {
|
||||||
|
// special-case empty json body, as it's a common client-side mistake
|
||||||
|
// TODO: maybe make this configurable or part of "strict" option
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = firstchar(body)
|
||||||
|
if (first !== '{' && first !== '[') {
|
||||||
|
debug('strict violation')
|
||||||
|
throw createStrictSyntaxError(body, first)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug('parse json')
|
||||||
|
return JSON.parse(body, reviver)
|
||||||
|
} catch (e) {
|
||||||
|
throw normalizeJsonSyntaxError(e, {
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function parse (body) {
|
||||||
|
if (body.length === 0) {
|
||||||
|
// special-case empty json body, as it's a common client-side mistake
|
||||||
|
// TODO: maybe make this configurable or part of "strict" option
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug('parse json')
|
||||||
|
return JSON.parse(body, reviver)
|
||||||
|
} catch (e) {
|
||||||
|
throw normalizeJsonSyntaxError(e, {
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create strict violation syntax error matching native error.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @param {string} char
|
||||||
|
* @returns {Error}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function createStrictSyntaxError (str, char) {
|
||||||
|
const index = str.indexOf(char)
|
||||||
|
let partial = ''
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
partial = str.substring(0, index) + JSON_SYNTAX_CHAR.repeat(str.length - index)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
|
||||||
|
} catch (e) {
|
||||||
|
return normalizeJsonSyntaxError(e, {
|
||||||
|
message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) {
|
||||||
|
return str.substring(index, index + placeholder.length)
|
||||||
|
}),
|
||||||
|
stack: e.stack
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first non-whitespace character in a string.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string|undefined}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function firstchar (str) {
|
||||||
|
const match = FIRST_CHAR_REGEXP.exec(str)
|
||||||
|
|
||||||
|
return match
|
||||||
|
? match[1]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a SyntaxError for JSON.parse.
|
||||||
|
*
|
||||||
|
* @param {SyntaxError} error
|
||||||
|
* @param {Object} obj
|
||||||
|
* @returns {SyntaxError}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function normalizeJsonSyntaxError (error, obj) {
|
||||||
|
const keys = Object.getOwnPropertyNames(error)
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i]
|
||||||
|
if (key !== 'stack' && key !== 'message') {
|
||||||
|
delete error[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace stack before message for Node.js 0.10 and below
|
||||||
|
error.stack = obj.stack.replace(error.message, obj.message)
|
||||||
|
error.message = obj.message
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = require('debug')('body-parser:raw')
|
||||||
|
const read = require('../read')
|
||||||
|
const { normalizeOptions, passthrough } = require('../utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = raw
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware to parse raw bodies.
|
||||||
|
*
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @returns {Function}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function raw (options) {
|
||||||
|
const normalizedOptions = normalizeOptions(options, 'application/octet-stream')
|
||||||
|
|
||||||
|
const readOptions = {
|
||||||
|
...normalizedOptions,
|
||||||
|
// Skip charset validation and parse the body as is
|
||||||
|
skipCharset: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return function rawParser (req, res, next) {
|
||||||
|
read(req, res, next, passthrough, debug, readOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = require('debug')('body-parser:text')
|
||||||
|
const read = require('../read')
|
||||||
|
const { normalizeOptions, passthrough } = require('../utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware to parse text bodies.
|
||||||
|
*
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @returns {Function}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function text (options) {
|
||||||
|
const normalizedOptions = normalizeOptions(options, 'text/plain')
|
||||||
|
|
||||||
|
return function textParser (req, res, next) {
|
||||||
|
read(req, res, next, passthrough, debug, normalizedOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*!
|
||||||
|
* body-parser
|
||||||
|
* Copyright(c) 2014 Jonathan Ong
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createError = require('http-errors')
|
||||||
|
const debug = require('debug')('body-parser:urlencoded')
|
||||||
|
const read = require('../read')
|
||||||
|
const qs = require('qs')
|
||||||
|
const { normalizeOptions } = require('../utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = urlencoded
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware to parse urlencoded bodies.
|
||||||
|
*
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @returns {Function}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function urlencoded (options) {
|
||||||
|
const normalizedOptions = normalizeOptions(options, 'application/x-www-form-urlencoded')
|
||||||
|
|
||||||
|
if (normalizedOptions.defaultCharset !== 'utf-8' && normalizedOptions.defaultCharset !== 'iso-8859-1') {
|
||||||
|
throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1')
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the appropriate query parser
|
||||||
|
const parse = createQueryParser(options)
|
||||||
|
|
||||||
|
const readOptions = {
|
||||||
|
...normalizedOptions,
|
||||||
|
// assert charset
|
||||||
|
isValidCharset: (charset) => charset === 'utf-8' || charset === 'iso-8859-1'
|
||||||
|
}
|
||||||
|
|
||||||
|
return function urlencodedParser (req, res, next) {
|
||||||
|
read(req, res, next, parse, debug, readOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extended query parser.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @returns {Function}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function createQueryParser (options) {
|
||||||
|
const extended = Boolean(options?.extended)
|
||||||
|
let parameterLimit = options?.parameterLimit !== undefined
|
||||||
|
? options?.parameterLimit
|
||||||
|
: 1000
|
||||||
|
const charsetSentinel = options?.charsetSentinel
|
||||||
|
const interpretNumericEntities = options?.interpretNumericEntities
|
||||||
|
const depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0
|
||||||
|
|
||||||
|
if (isNaN(parameterLimit) || parameterLimit < 1) {
|
||||||
|
throw new TypeError('option parameterLimit must be a positive number')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(depth) || depth < 0) {
|
||||||
|
throw new TypeError('option depth must be a zero or a positive number')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFinite(parameterLimit)) {
|
||||||
|
parameterLimit = parameterLimit | 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return function parse (body, encoding) {
|
||||||
|
if (!body.length) return {}
|
||||||
|
|
||||||
|
const paramCount = parameterCount(body, parameterLimit)
|
||||||
|
|
||||||
|
if (paramCount === undefined) {
|
||||||
|
debug('too many parameters')
|
||||||
|
throw createError(413, 'too many parameters', {
|
||||||
|
type: 'parameters.too.many'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayLimit = extended ? Math.max(100, paramCount) : paramCount
|
||||||
|
|
||||||
|
debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding')
|
||||||
|
try {
|
||||||
|
return qs.parse(body, {
|
||||||
|
allowPrototypes: true,
|
||||||
|
arrayLimit: arrayLimit,
|
||||||
|
depth: depth,
|
||||||
|
charsetSentinel: charsetSentinel,
|
||||||
|
interpretNumericEntities: interpretNumericEntities,
|
||||||
|
charset: encoding,
|
||||||
|
parameterLimit: parameterLimit,
|
||||||
|
strictDepth: true
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof RangeError) {
|
||||||
|
throw createError(400, 'The input exceeded the depth', {
|
||||||
|
type: 'querystring.parse.rangeError'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of parameters, stopping once limit reached
|
||||||
|
*
|
||||||
|
* @param {string} body
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {number|undefined} Returns undefined if limit exceeded
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function parameterCount (body, limit) {
|
||||||
|
let count = 0
|
||||||
|
let index = -1
|
||||||
|
do {
|
||||||
|
count++
|
||||||
|
if (count > limit) return undefined // Early exit if limit exceeded
|
||||||
|
index = body.indexOf('&', index + 1)
|
||||||
|
} while (index !== -1)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const bytes = require('bytes')
|
||||||
|
const contentType = require('content-type')
|
||||||
|
const typeis = require('type-is')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
getCharset,
|
||||||
|
normalizeOptions,
|
||||||
|
passthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the charset of a request.
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @returns {string | undefined}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function getCharset (req) {
|
||||||
|
const header = req.headers['content-type']
|
||||||
|
if (!header) return undefined
|
||||||
|
return contentType.parse(header).parameters.charset?.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the simple type checker.
|
||||||
|
*
|
||||||
|
* @param {string | string[]} type
|
||||||
|
* @returns {Function}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function typeChecker (type) {
|
||||||
|
return function checkType (req) {
|
||||||
|
return Boolean(typeis(req, type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the common options for all parsers.
|
||||||
|
*
|
||||||
|
* @param {Object} options options to normalize
|
||||||
|
* @param {string | string[] | Function} defaultType default content type(s) or a function to determine it
|
||||||
|
* @returns {Object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function normalizeOptions (options, defaultType) {
|
||||||
|
if (!defaultType) {
|
||||||
|
// Parsers must define a default content type
|
||||||
|
throw new TypeError('defaultType must be provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
const inflate = options?.inflate !== false
|
||||||
|
const limit = typeof options?.limit === 'undefined' || options?.limit === null
|
||||||
|
? 102400 // 100kb default
|
||||||
|
: bytes.parse(options.limit)
|
||||||
|
const type = options?.type || defaultType
|
||||||
|
const verify = options?.verify || false
|
||||||
|
const defaultCharset = options?.defaultCharset || 'utf-8'
|
||||||
|
|
||||||
|
if (limit === null) {
|
||||||
|
throw new TypeError(`option limit "${String(options.limit)}" is invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verify !== false && typeof verify !== 'function') {
|
||||||
|
throw new TypeError('option verify must be function')
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the appropriate type checking function
|
||||||
|
const shouldParse = typeof type !== 'function'
|
||||||
|
? typeChecker(type)
|
||||||
|
: type
|
||||||
|
|
||||||
|
return {
|
||||||
|
inflate,
|
||||||
|
limit,
|
||||||
|
verify,
|
||||||
|
defaultCharset,
|
||||||
|
shouldParse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passthrough function that returns input unchanged.
|
||||||
|
* Used by parsers that don't need to transform the data.
|
||||||
|
*
|
||||||
|
* @param {*} value
|
||||||
|
* @returns {*}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function passthrough (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Douglas Christopher Wilson
|
||||||
|
|
||||||
|
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,69 @@
|
||||||
|
# content-type
|
||||||
|
|
||||||
|
[![NPM version][npm-image]][npm-url]
|
||||||
|
[![NPM downloads][downloads-image]][downloads-url]
|
||||||
|
[![Build status][build-image]][build-url]
|
||||||
|
[![Build coverage][coverage-image]][coverage-url]
|
||||||
|
[![License][license-image]][license-url]
|
||||||
|
|
||||||
|
Create and parse HTTP `Content-Type` header.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install content-type
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
const contentType = require("content-type");
|
||||||
|
```
|
||||||
|
|
||||||
|
### contentType.parse(string, options?)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const obj = contentType.parse("image/svg+xml; charset=utf-8");
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse a `Content-Type` header. This will return an object with the following properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`):
|
||||||
|
|
||||||
|
- `type`: The media type. Example: `'image/svg+xml'`.
|
||||||
|
- `parameters`: An object of the parameters in the media type (parameter name is always lower case). Example: `{charset: 'utf-8'}`.
|
||||||
|
|
||||||
|
The parser is lenient and does not error. You should validate `type` and `parameters` before trusting them.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
- `parameters` (default: `true`): Set to `false` to skip parameters.
|
||||||
|
|
||||||
|
### contentType.format(obj)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const str = contentType.format({
|
||||||
|
type: "image/svg+xml",
|
||||||
|
parameters: { charset: "utf-8" },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Format an object into a `Content-Type` header. This will return a string of the content type for the given object with the following properties (examples are shown that produce the string `'image/svg+xml; charset=utf-8'`):
|
||||||
|
|
||||||
|
- `type`: The media type. Example: `'image/svg+xml'`.
|
||||||
|
- `parameters`: An optional object of the parameters in the media type. Example: `{charset: 'utf-8'}`.
|
||||||
|
|
||||||
|
Throws a `TypeError` if the object contains an invalid type or parameter names.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[npm-image]: https://img.shields.io/npm/v/content-type
|
||||||
|
[npm-url]: https://npmjs.org/package/content-type
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/content-type
|
||||||
|
[downloads-url]: https://npmjs.org/package/content-type
|
||||||
|
[build-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-type/ci.yml?branch=master
|
||||||
|
[build-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml?query=branch%3Amaster
|
||||||
|
[coverage-image]: https://img.shields.io/codecov/c/gh/jshttp/content-type
|
||||||
|
[coverage-url]: https://codecov.io/gh/jshttp/content-type
|
||||||
|
[license-image]: http://img.shields.io/npm/l/content-type.svg?style=flat
|
||||||
|
[license-url]: LICENSE
|
||||||
26
node_modules/body-parser/node_modules/content-type/dist/index.d.ts
generated
vendored
Normal file
26
node_modules/body-parser/node_modules/content-type/dist/index.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*!
|
||||||
|
* content-type
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The content type object contains a type string and optional parameters.
|
||||||
|
*/
|
||||||
|
export interface ContentType {
|
||||||
|
type: string;
|
||||||
|
parameters: Record<string, string>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Format an object into a `Content-Type` header.
|
||||||
|
*/
|
||||||
|
export declare function format(obj: Partial<ContentType>): string;
|
||||||
|
/**
|
||||||
|
* Options for parsing a `Content-Type` header.
|
||||||
|
*/
|
||||||
|
export interface ParseOptions {
|
||||||
|
parameters?: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a `Content-Type` header.
|
||||||
|
*/
|
||||||
|
export declare function parse(header: string, options?: ParseOptions): ContentType;
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
"use strict";
|
||||||
|
/*!
|
||||||
|
* content-type
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.format = format;
|
||||||
|
exports.parse = parse;
|
||||||
|
const TEXT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]*$/;
|
||||||
|
const TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
||||||
|
/**
|
||||||
|
* RegExp to match chars that must be quoted-pair in RFC 9110 sec 5.6.4
|
||||||
|
*/
|
||||||
|
const QUOTE_REGEXP = /[\\"]/g;
|
||||||
|
/**
|
||||||
|
* RegExp to match type in RFC 9110 sec 8.3.1
|
||||||
|
*
|
||||||
|
* media-type = type "/" subtype
|
||||||
|
* type = token
|
||||||
|
* subtype = token
|
||||||
|
*/
|
||||||
|
const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
||||||
|
/**
|
||||||
|
* Null object perf optimization. Faster than `Object.create(null)` and `{ __proto__: null }`.
|
||||||
|
*/
|
||||||
|
const NullObject = /* @__PURE__ */ (() => {
|
||||||
|
const C = function () { };
|
||||||
|
C.prototype = Object.create(null);
|
||||||
|
return C;
|
||||||
|
})();
|
||||||
|
/**
|
||||||
|
* Format an object into a `Content-Type` header.
|
||||||
|
*/
|
||||||
|
function format(obj) {
|
||||||
|
const { type, parameters } = obj;
|
||||||
|
if (!type || !TYPE_REGEXP.test(type)) {
|
||||||
|
throw new TypeError(`Invalid type: ${type}`);
|
||||||
|
}
|
||||||
|
let result = type;
|
||||||
|
if (parameters) {
|
||||||
|
for (const param of Object.keys(parameters)) {
|
||||||
|
if (!TOKEN_REGEXP.test(param)) {
|
||||||
|
throw new TypeError(`Invalid parameter name: ${param}`);
|
||||||
|
}
|
||||||
|
result += `; ${param}=${qstring(parameters[param])}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse a `Content-Type` header.
|
||||||
|
*/
|
||||||
|
function parse(header, options) {
|
||||||
|
const len = header.length;
|
||||||
|
let index = skipOWS(header, 0, len);
|
||||||
|
const valueStart = index;
|
||||||
|
index = skipValue(header, index, len);
|
||||||
|
const valueEnd = trailingOWS(header, valueStart, index);
|
||||||
|
const type = header.slice(valueStart, valueEnd).toLowerCase();
|
||||||
|
const parameters = options?.parameters === false
|
||||||
|
? new NullObject()
|
||||||
|
: parseParameters(header, index, len);
|
||||||
|
return { type, parameters };
|
||||||
|
}
|
||||||
|
const SP = 32; // " "
|
||||||
|
const HTAB = 9; // "\t"
|
||||||
|
const SEMI = 59; // ";"
|
||||||
|
const EQ = 61; // "="
|
||||||
|
const DQUOTE = 34; // '"'
|
||||||
|
const BSLASH = 92; // "\\"
|
||||||
|
/**
|
||||||
|
* Parses the parameters of a `Content-Type` header starting at the given index.
|
||||||
|
*/
|
||||||
|
function parseParameters(header, index, len) {
|
||||||
|
const parameters = new NullObject();
|
||||||
|
parameter: while (index < len) {
|
||||||
|
index = skipOWS(header, index + 1 /* Skip over ; */, len);
|
||||||
|
const keyStart = index;
|
||||||
|
while (index < len) {
|
||||||
|
const code = header.charCodeAt(index);
|
||||||
|
if (code === SEMI)
|
||||||
|
continue parameter;
|
||||||
|
if (code === EQ) {
|
||||||
|
const keyEnd = trailingOWS(header, keyStart, index);
|
||||||
|
const key = header.slice(keyStart, keyEnd).toLowerCase();
|
||||||
|
index = skipOWS(header, index + 1, len);
|
||||||
|
if (index < len && header.charCodeAt(index) === DQUOTE) {
|
||||||
|
index++;
|
||||||
|
let value = "";
|
||||||
|
while (index < len) {
|
||||||
|
const code = header.charCodeAt(index++);
|
||||||
|
if (code === DQUOTE) {
|
||||||
|
index = skipValue(header, index, len);
|
||||||
|
if (parameters[key] === undefined)
|
||||||
|
parameters[key] = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (code === BSLASH && index < len) {
|
||||||
|
value += header[index++];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value += String.fromCharCode(code);
|
||||||
|
}
|
||||||
|
continue parameter;
|
||||||
|
}
|
||||||
|
const valueStart = index;
|
||||||
|
index = skipValue(header, index, len);
|
||||||
|
if (parameters[key] === undefined) {
|
||||||
|
const valueEnd = trailingOWS(header, valueStart, index);
|
||||||
|
parameters[key] = header.slice(valueStart, valueEnd);
|
||||||
|
}
|
||||||
|
continue parameter;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Skip over characters until a semicolon.
|
||||||
|
*/
|
||||||
|
function skipValue(str, index, len) {
|
||||||
|
while (index < len) {
|
||||||
|
const char = str.charCodeAt(index);
|
||||||
|
if (char === SEMI)
|
||||||
|
break;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Skip optional whitespace (OWS) in an HTTP header value.
|
||||||
|
*
|
||||||
|
* OWS is defined in RFC 9110 sec 5.6.3 as SP (" ") or HTAB ("\t").
|
||||||
|
*/
|
||||||
|
function skipOWS(header, index, len) {
|
||||||
|
while (index < len) {
|
||||||
|
const char = header.charCodeAt(index);
|
||||||
|
if (char !== SP && char !== HTAB)
|
||||||
|
break;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Trim optional whitespace (OWS) from the end of a substring.
|
||||||
|
*
|
||||||
|
* OWS is defined in RFC 9110 sec 5.6.3 as SP (" ") or HTAB ("\t").
|
||||||
|
*/
|
||||||
|
function trailingOWS(header, start, end) {
|
||||||
|
while (end > start) {
|
||||||
|
const char = header.charCodeAt(end - 1);
|
||||||
|
if (char !== SP && char !== HTAB)
|
||||||
|
break;
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Serialize a parameter value.
|
||||||
|
*/
|
||||||
|
function qstring(str) {
|
||||||
|
if (TOKEN_REGEXP.test(str))
|
||||||
|
return str;
|
||||||
|
if (TEXT_REGEXP.test(str))
|
||||||
|
return `"${str.replace(QUOTE_REGEXP, "\\$&")}"`;
|
||||||
|
throw new TypeError(`Invalid parameter value: ${str}`);
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
node_modules/body-parser/node_modules/content-type/dist/index.js.map
generated
vendored
Normal file
1
node_modules/body-parser/node_modules/content-type/dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "content-type",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Create and parse HTTP Content-Type header",
|
||||||
|
"keywords": [
|
||||||
|
"content-type",
|
||||||
|
"http",
|
||||||
|
"req",
|
||||||
|
"res",
|
||||||
|
"rfc7231",
|
||||||
|
"rfc9110"
|
||||||
|
],
|
||||||
|
"repository": "jshttp/content-type",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"type": "commonjs",
|
||||||
|
"exports": "./dist/index.js",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"typings": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist/"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"bench": "vitest bench",
|
||||||
|
"build": "ts-scripts build",
|
||||||
|
"format": "ts-scripts format",
|
||||||
|
"prepare": "ts-scripts install && npm run build",
|
||||||
|
"specs": "ts-scripts specs",
|
||||||
|
"test": "ts-scripts test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@borderless/ts-scripts": "^0.15.0",
|
||||||
|
"@vitest/coverage-v8": "^3.0.5",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"ts-scripts": {
|
||||||
|
"dist": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"project": [
|
||||||
|
"tsconfig.build.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
"name": "body-parser",
|
||||||
|
"description": "Node.js body parsing middleware",
|
||||||
|
"version": "2.3.0",
|
||||||
|
"contributors": [
|
||||||
|
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"Jonathan Ong <me@jongleberry.com> (http://jongleberry.com)"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "expressjs/body-parser",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
},
|
||||||
|
"type": "commonjs",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
"./json": "./lib/types/json.js",
|
||||||
|
"./raw": "./lib/types/raw.js",
|
||||||
|
"./text": "./lib/types/text.js",
|
||||||
|
"./urlencoded": "./lib/types/urlencoded.js",
|
||||||
|
"./lib/*": "./lib/*.js",
|
||||||
|
"./lib/*.js": "./lib/*.js",
|
||||||
|
"./lib/types/*": "./lib/types/*.js",
|
||||||
|
"./lib/types/*.js": "./lib/types/*.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^2.0.0",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"iconv-lite": "^0.7.2",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.15.2",
|
||||||
|
"raw-body": "^3.0.2",
|
||||||
|
"type-is": "^2.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-standard": "^14.1.1",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-markdown": "^3.0.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^6.6.0",
|
||||||
|
"eslint-plugin-standard": "^4.1.0",
|
||||||
|
"mocha": "^11.7.6",
|
||||||
|
"nyc": "^17.1.0",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"LICENSE",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --reporter spec --check-leaks test/",
|
||||||
|
"test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
3.1.2 / 2022-01-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix return value for un-parsable strings
|
||||||
|
|
||||||
|
3.1.1 / 2021-11-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix "thousandsSeparator" incorrecting formatting fractional part
|
||||||
|
|
||||||
|
3.1.0 / 2019-01-22
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add petabyte (`pb`) support
|
||||||
|
|
||||||
|
3.0.0 / 2017-08-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Change "kB" to "KB" in format output
|
||||||
|
* Remove support for Node.js 0.6
|
||||||
|
* Remove support for ComponentJS
|
||||||
|
|
||||||
|
2.5.0 / 2017-03-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add option "unit"
|
||||||
|
|
||||||
|
2.4.0 / 2016-06-01
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add option "unitSeparator"
|
||||||
|
|
||||||
|
2.3.0 / 2016-02-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Drop partial bytes on all parsed units
|
||||||
|
* Fix non-finite numbers to `.format` to return `null`
|
||||||
|
* Fix parsing byte string that looks like hex
|
||||||
|
* perf: hoist regular expressions
|
||||||
|
|
||||||
|
2.2.0 / 2015-11-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add option "decimalPlaces"
|
||||||
|
* add option "fixedDecimals"
|
||||||
|
|
||||||
|
2.1.0 / 2015-05-21
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add `.format` export
|
||||||
|
* add `.parse` export
|
||||||
|
|
||||||
|
2.0.2 / 2015-05-20
|
||||||
|
==================
|
||||||
|
|
||||||
|
* remove map recreation
|
||||||
|
* remove unnecessary object construction
|
||||||
|
|
||||||
|
2.0.1 / 2015-05-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix browserify require
|
||||||
|
* remove node.extend dependency
|
||||||
|
|
||||||
|
2.0.0 / 2015-04-12
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add option "case"
|
||||||
|
* add option "thousandsSeparator"
|
||||||
|
* return "null" on invalid parse input
|
||||||
|
* support proper round-trip: bytes(bytes(num)) === num
|
||||||
|
* units no longer case sensitive when parsing
|
||||||
|
|
||||||
|
1.0.0 / 2014-05-05
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add negative support. fixes #6
|
||||||
|
|
||||||
|
0.3.0 / 2014-03-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* added terabyte support
|
||||||
|
|
||||||
|
0.2.1 / 2013-04-01
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add .component
|
||||||
|
|
||||||
|
0.2.0 / 2012-10-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* bytes(200).should.eql('200b')
|
||||||
|
|
||||||
|
0.1.0 / 2012-07-04
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add bytes to string conversion [yields]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2012-2014 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
Copyright (c) 2015 Jed Watson <jed.watson@me.com>
|
||||||
|
|
||||||
|
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,152 @@
|
||||||
|
# Bytes utility
|
||||||
|
|
||||||
|
[![NPM Version][npm-image]][npm-url]
|
||||||
|
[![NPM Downloads][downloads-image]][downloads-url]
|
||||||
|
[![Build Status][ci-image]][ci-url]
|
||||||
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||||
|
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||||
|
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
var bytes = require('bytes');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### bytes(number|string value, [options]): number|string|null
|
||||||
|
|
||||||
|
Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|----------|--------------------|
|
||||||
|
| value | `number`|`string` | Number value to format or string value to parse |
|
||||||
|
| options | `Object` | Conversion options for `format` |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|------------------|-------------------------------------------------|
|
||||||
|
| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. |
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
bytes(1024);
|
||||||
|
// output: '1KB'
|
||||||
|
|
||||||
|
bytes('1KB');
|
||||||
|
// output: 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
#### bytes.format(number value, [options]): string|null
|
||||||
|
|
||||||
|
Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is
|
||||||
|
rounded.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|----------|--------------------|
|
||||||
|
| value | `number` | Value in bytes |
|
||||||
|
| options | `Object` | Conversion options |
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|-------------------|--------|-----------------------------------------------------------------------------------------|
|
||||||
|
| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. |
|
||||||
|
| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` |
|
||||||
|
| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. |
|
||||||
|
| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). |
|
||||||
|
| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|------------------|-------------------------------------------------|
|
||||||
|
| results | `string`|`null` | Return null upon error. String value otherwise. |
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
bytes.format(1024);
|
||||||
|
// output: '1KB'
|
||||||
|
|
||||||
|
bytes.format(1000);
|
||||||
|
// output: '1000B'
|
||||||
|
|
||||||
|
bytes.format(1000, {thousandsSeparator: ' '});
|
||||||
|
// output: '1 000B'
|
||||||
|
|
||||||
|
bytes.format(1024 * 1.7, {decimalPlaces: 0});
|
||||||
|
// output: '2KB'
|
||||||
|
|
||||||
|
bytes.format(1024, {unitSeparator: ' '});
|
||||||
|
// output: '1 KB'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### bytes.parse(string|number value): number|null
|
||||||
|
|
||||||
|
Parse the string value into an integer in bytes. If no unit is given, or `value`
|
||||||
|
is a number, it is assumed the value is in bytes.
|
||||||
|
|
||||||
|
Supported units and abbreviations are as follows and are case-insensitive:
|
||||||
|
|
||||||
|
* `b` for bytes
|
||||||
|
* `kb` for kilobytes
|
||||||
|
* `mb` for megabytes
|
||||||
|
* `gb` for gigabytes
|
||||||
|
* `tb` for terabytes
|
||||||
|
* `pb` for petabytes
|
||||||
|
|
||||||
|
The units are in powers of two, not ten. This means 1kb = 1024b according to this parser.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------------|--------|--------------------|
|
||||||
|
| value | `string`|`number` | String to parse, or number in bytes. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|-------------|-------------------------|
|
||||||
|
| results | `number`|`null` | Return null upon error. Value in bytes otherwise. |
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
bytes.parse('1KB');
|
||||||
|
// output: 1024
|
||||||
|
|
||||||
|
bytes.parse('1024');
|
||||||
|
// output: 1024
|
||||||
|
|
||||||
|
bytes.parse(1024);
|
||||||
|
// output: 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci
|
||||||
|
[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master
|
||||||
|
[downloads-image]: https://badgen.net/npm/dm/bytes
|
||||||
|
[downloads-url]: https://npmjs.org/package/bytes
|
||||||
|
[npm-image]: https://badgen.net/npm/v/bytes
|
||||||
|
[npm-url]: https://npmjs.org/package/bytes
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*!
|
||||||
|
* bytes
|
||||||
|
* Copyright(c) 2012-2014 TJ Holowaychuk
|
||||||
|
* Copyright(c) 2015 Jed Watson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = bytes;
|
||||||
|
module.exports.format = format;
|
||||||
|
module.exports.parse = parse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module variables.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g;
|
||||||
|
|
||||||
|
var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/;
|
||||||
|
|
||||||
|
var map = {
|
||||||
|
b: 1,
|
||||||
|
kb: 1 << 10,
|
||||||
|
mb: 1 << 20,
|
||||||
|
gb: 1 << 30,
|
||||||
|
tb: Math.pow(1024, 4),
|
||||||
|
pb: Math.pow(1024, 5),
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given value in bytes into a string or parse to string to an integer in bytes.
|
||||||
|
*
|
||||||
|
* @param {string|number} value
|
||||||
|
* @param {{
|
||||||
|
* case: [string],
|
||||||
|
* decimalPlaces: [number]
|
||||||
|
* fixedDecimals: [boolean]
|
||||||
|
* thousandsSeparator: [string]
|
||||||
|
* unitSeparator: [string]
|
||||||
|
* }} [options] bytes options.
|
||||||
|
*
|
||||||
|
* @returns {string|number|null}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function bytes(value, options) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return format(value, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the given value in bytes into a string.
|
||||||
|
*
|
||||||
|
* If the value is negative, it is kept as such. If it is a float,
|
||||||
|
* it is rounded.
|
||||||
|
*
|
||||||
|
* @param {number} value
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [options.decimalPlaces=2]
|
||||||
|
* @param {number} [options.fixedDecimals=false]
|
||||||
|
* @param {string} [options.thousandsSeparator=]
|
||||||
|
* @param {string} [options.unit=]
|
||||||
|
* @param {string} [options.unitSeparator=]
|
||||||
|
*
|
||||||
|
* @returns {string|null}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function format(value, options) {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var mag = Math.abs(value);
|
||||||
|
var thousandsSeparator = (options && options.thousandsSeparator) || '';
|
||||||
|
var unitSeparator = (options && options.unitSeparator) || '';
|
||||||
|
var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2;
|
||||||
|
var fixedDecimals = Boolean(options && options.fixedDecimals);
|
||||||
|
var unit = (options && options.unit) || '';
|
||||||
|
|
||||||
|
if (!unit || !map[unit.toLowerCase()]) {
|
||||||
|
if (mag >= map.pb) {
|
||||||
|
unit = 'PB';
|
||||||
|
} else if (mag >= map.tb) {
|
||||||
|
unit = 'TB';
|
||||||
|
} else if (mag >= map.gb) {
|
||||||
|
unit = 'GB';
|
||||||
|
} else if (mag >= map.mb) {
|
||||||
|
unit = 'MB';
|
||||||
|
} else if (mag >= map.kb) {
|
||||||
|
unit = 'KB';
|
||||||
|
} else {
|
||||||
|
unit = 'B';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = value / map[unit.toLowerCase()];
|
||||||
|
var str = val.toFixed(decimalPlaces);
|
||||||
|
|
||||||
|
if (!fixedDecimals) {
|
||||||
|
str = str.replace(formatDecimalsRegExp, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thousandsSeparator) {
|
||||||
|
str = str.split('.').map(function (s, i) {
|
||||||
|
return i === 0
|
||||||
|
? s.replace(formatThousandsRegExp, thousandsSeparator)
|
||||||
|
: s
|
||||||
|
}).join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return str + unitSeparator + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the string value into an integer in bytes.
|
||||||
|
*
|
||||||
|
* If no unit is given, it is assumed the value is in bytes.
|
||||||
|
*
|
||||||
|
* @param {number|string} val
|
||||||
|
*
|
||||||
|
* @returns {number|null}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function parse(val) {
|
||||||
|
if (typeof val === 'number' && !isNaN(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof val !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if the string passed is valid
|
||||||
|
var results = parseRegExp.exec(val);
|
||||||
|
var floatValue;
|
||||||
|
var unit = 'b';
|
||||||
|
|
||||||
|
if (!results) {
|
||||||
|
// Nothing could be extracted from the given string
|
||||||
|
floatValue = parseInt(val, 10);
|
||||||
|
unit = 'b'
|
||||||
|
} else {
|
||||||
|
// Retrieve the value and the unit
|
||||||
|
floatValue = parseFloat(results[1]);
|
||||||
|
unit = results[4].toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(floatValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(map[unit] * floatValue);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "bytes",
|
||||||
|
"description": "Utility to parse a string bytes to bytes and vice-versa",
|
||||||
|
"version": "3.1.2",
|
||||||
|
"author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)",
|
||||||
|
"contributors": [
|
||||||
|
"Jed Watson <jed.watson@me.com>",
|
||||||
|
"Théo FIDRY <theo.fidry@gmail.com>"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"byte",
|
||||||
|
"bytes",
|
||||||
|
"utility",
|
||||||
|
"parse",
|
||||||
|
"parser",
|
||||||
|
"convert",
|
||||||
|
"converter"
|
||||||
|
],
|
||||||
|
"repository": "visionmedia/bytes.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "7.32.0",
|
||||||
|
"eslint-plugin-markdown": "2.2.1",
|
||||||
|
"mocha": "9.2.0",
|
||||||
|
"nyc": "15.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"History.md",
|
||||||
|
"LICENSE",
|
||||||
|
"Readme.md",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --check-leaks --reporter spec",
|
||||||
|
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
|
||||||
|
"extends": "@ljharb",
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"func-name-matching": 0,
|
||||||
|
"id-length": 0,
|
||||||
|
"new-cap": [2, {
|
||||||
|
"capIsNewExceptions": [
|
||||||
|
"GetIntrinsic",
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
"no-extra-parens": 0,
|
||||||
|
"no-magic-numbers": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [ljharb]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: npm/call-bind-apply-helpers
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"all": true,
|
||||||
|
"check-coverage": false,
|
||||||
|
"reporter": ["text-summary", "text", "html", "json"],
|
||||||
|
"exclude": [
|
||||||
|
"coverage",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1)
|
||||||
|
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1)
|
||||||
|
|
||||||
|
## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8)
|
||||||
|
- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75)
|
||||||
|
- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940)
|
||||||
|
|
||||||
|
## v1.0.0 - 2024-12-05
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04)
|
||||||
|
- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f)
|
||||||
|
- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603)
|
||||||
|
- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Jordan Harband
|
||||||
|
|
||||||
|
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,62 @@
|
||||||
|
# call-bind-apply-helpers <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||||
|
|
||||||
|
[![github actions][actions-image]][actions-url]
|
||||||
|
[![coverage][codecov-image]][codecov-url]
|
||||||
|
[![dependency status][deps-svg]][deps-url]
|
||||||
|
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
||||||
|
[![License][license-image]][license-url]
|
||||||
|
[![Downloads][downloads-image]][downloads-url]
|
||||||
|
|
||||||
|
[![npm badge][npm-badge-png]][package-url]
|
||||||
|
|
||||||
|
Helper functions around Function call/apply/bind, for use in `call-bind`.
|
||||||
|
|
||||||
|
The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`.
|
||||||
|
Please use `call-bind` unless you have a very good reason not to.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save call-bind-apply-helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage/Examples
|
||||||
|
|
||||||
|
```js
|
||||||
|
const assert = require('assert');
|
||||||
|
const callBindBasic = require('call-bind-apply-helpers');
|
||||||
|
|
||||||
|
function f(a, b) {
|
||||||
|
assert.equal(this, 1);
|
||||||
|
assert.equal(a, 2);
|
||||||
|
assert.equal(b, 3);
|
||||||
|
assert.equal(arguments.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fBound = callBindBasic([f, 1]);
|
||||||
|
|
||||||
|
delete Function.prototype.call;
|
||||||
|
delete Function.prototype.bind;
|
||||||
|
|
||||||
|
fBound(2, 3);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Clone the repo, `npm install`, and run `npm test`
|
||||||
|
|
||||||
|
[package-url]: https://npmjs.org/package/call-bind-apply-helpers
|
||||||
|
[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg
|
||||||
|
[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg
|
||||||
|
[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers
|
||||||
|
[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg
|
||||||
|
[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies
|
||||||
|
[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true
|
||||||
|
[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg
|
||||||
|
[license-url]: LICENSE
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg
|
||||||
|
[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers
|
||||||
|
[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg
|
||||||
|
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/
|
||||||
|
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers
|
||||||
|
[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export = Reflect.apply;
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var bind = require('function-bind');
|
||||||
|
|
||||||
|
var $apply = require('./functionApply');
|
||||||
|
var $call = require('./functionCall');
|
||||||
|
var $reflectApply = require('./reflectApply');
|
||||||
|
|
||||||
|
/** @type {import('./actualApply')} */
|
||||||
|
module.exports = $reflectApply || bind.call($call, $apply);
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import actualApply from './actualApply';
|
||||||
|
|
||||||
|
type TupleSplitHead<T extends any[], N extends number> = T['length'] extends N
|
||||||
|
? T
|
||||||
|
: T extends [...infer R, any]
|
||||||
|
? TupleSplitHead<R, N>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type TupleSplitTail<T, N extends number, O extends any[] = []> = O['length'] extends N
|
||||||
|
? T
|
||||||
|
: T extends [infer F, ...infer R]
|
||||||
|
? TupleSplitTail<[...R], N, [...O, F]>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type TupleSplit<T extends any[], N extends number> = [TupleSplitHead<T, N>, TupleSplitTail<T, N>]
|
||||||
|
|
||||||
|
declare function applyBind(...args: TupleSplit<Parameters<typeof actualApply>, 2>[1]): ReturnType<typeof actualApply>;
|
||||||
|
|
||||||
|
export = applyBind;
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var bind = require('function-bind');
|
||||||
|
var $apply = require('./functionApply');
|
||||||
|
var actualApply = require('./actualApply');
|
||||||
|
|
||||||
|
/** @type {import('./applyBind')} */
|
||||||
|
module.exports = function applyBind() {
|
||||||
|
return actualApply(bind, $apply, arguments);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export = Function.prototype.apply;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('./functionApply')} */
|
||||||
|
module.exports = Function.prototype.apply;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export = Function.prototype.call;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('./functionCall')} */
|
||||||
|
module.exports = Function.prototype.call;
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
type RemoveFromTuple<
|
||||||
|
Tuple extends readonly unknown[],
|
||||||
|
RemoveCount extends number,
|
||||||
|
Index extends 1[] = []
|
||||||
|
> = Index["length"] extends RemoveCount
|
||||||
|
? Tuple
|
||||||
|
: Tuple extends [infer First, ...infer Rest]
|
||||||
|
? RemoveFromTuple<Rest, RemoveCount, [...Index, 1]>
|
||||||
|
: Tuple;
|
||||||
|
|
||||||
|
type ConcatTuples<
|
||||||
|
Prefix extends readonly unknown[],
|
||||||
|
Suffix extends readonly unknown[]
|
||||||
|
> = [...Prefix, ...Suffix];
|
||||||
|
|
||||||
|
type ExtractFunctionParams<T> = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R
|
||||||
|
? { thisArg: TThis; params: P; returnType: R }
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type BindFunction<
|
||||||
|
T extends (this: any, ...args: any[]) => any,
|
||||||
|
TThis,
|
||||||
|
TBoundArgs extends readonly unknown[],
|
||||||
|
ReceiverBound extends boolean
|
||||||
|
> = ExtractFunctionParams<T> extends {
|
||||||
|
thisArg: infer OrigThis;
|
||||||
|
params: infer P extends readonly unknown[];
|
||||||
|
returnType: infer R;
|
||||||
|
}
|
||||||
|
? ReceiverBound extends true
|
||||||
|
? (...args: RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>) => R extends [OrigThis, ...infer Rest]
|
||||||
|
? [TThis, ...Rest] // Replace `this` with `thisArg`
|
||||||
|
: R
|
||||||
|
: <U, RemainingArgs extends RemoveFromTuple<P, Extract<TBoundArgs["length"], number>>>(
|
||||||
|
thisArg: U,
|
||||||
|
...args: RemainingArgs
|
||||||
|
) => R extends [OrigThis, ...infer Rest]
|
||||||
|
? [U, ...ConcatTuples<TBoundArgs, Rest>] // Preserve bound args in return type
|
||||||
|
: R
|
||||||
|
: never;
|
||||||
|
|
||||||
|
declare function callBind<
|
||||||
|
const T extends (this: any, ...args: any[]) => any,
|
||||||
|
Extracted extends ExtractFunctionParams<T>,
|
||||||
|
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[],
|
||||||
|
const TThis extends Extracted["thisArg"]
|
||||||
|
>(
|
||||||
|
args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs]
|
||||||
|
): BindFunction<T, TThis, TBoundArgs, true>;
|
||||||
|
|
||||||
|
declare function callBind<
|
||||||
|
const T extends (this: any, ...args: any[]) => any,
|
||||||
|
Extracted extends ExtractFunctionParams<T>,
|
||||||
|
const TBoundArgs extends Partial<Extracted["params"]> & readonly unknown[]
|
||||||
|
>(
|
||||||
|
args: [fn: T, ...boundArgs: TBoundArgs]
|
||||||
|
): BindFunction<T, Extracted["thisArg"], TBoundArgs, false>;
|
||||||
|
|
||||||
|
declare function callBind<const TArgs extends readonly unknown[]>(
|
||||||
|
args: [fn: Exclude<TArgs[0], Function>, ...rest: TArgs]
|
||||||
|
): never;
|
||||||
|
|
||||||
|
// export as namespace callBind;
|
||||||
|
export = callBind;
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var bind = require('function-bind');
|
||||||
|
var $TypeError = require('es-errors/type');
|
||||||
|
|
||||||
|
var $call = require('./functionCall');
|
||||||
|
var $actualApply = require('./actualApply');
|
||||||
|
|
||||||
|
/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */
|
||||||
|
module.exports = function callBindBasic(args) {
|
||||||
|
if (args.length < 1 || typeof args[0] !== 'function') {
|
||||||
|
throw new $TypeError('a function is required');
|
||||||
|
}
|
||||||
|
return $actualApply(bind, $call, args);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"name": "call-bind-apply-helpers",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"description": "Helper functions around Function call/apply/bind, for use in `call-bind`",
|
||||||
|
"main": "index.js",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./actualApply": "./actualApply.js",
|
||||||
|
"./applyBind": "./applyBind.js",
|
||||||
|
"./functionApply": "./functionApply.js",
|
||||||
|
"./functionCall": "./functionCall.js",
|
||||||
|
"./reflectApply": "./reflectApply.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepack": "npmignore --auto --commentLines=auto",
|
||||||
|
"prepublish": "not-in-publish || npm run prepublishOnly",
|
||||||
|
"prepublishOnly": "safe-publish-latest",
|
||||||
|
"prelint": "evalmd README.md",
|
||||||
|
"lint": "eslint --ext=.js,.mjs .",
|
||||||
|
"postlint": "tsc -p . && attw -P",
|
||||||
|
"pretest": "npm run lint",
|
||||||
|
"tests-only": "nyc tape 'test/**/*.js'",
|
||||||
|
"test": "npm run tests-only",
|
||||||
|
"posttest": "npx npm@'>=10.2' audit --production",
|
||||||
|
"version": "auto-changelog && git add CHANGELOG.md",
|
||||||
|
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ljharb/call-bind-apply-helpers.git"
|
||||||
|
},
|
||||||
|
"author": "Jordan Harband <ljharb@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ljharb/call-bind-apply-helpers/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@arethetypeswrong/cli": "^0.17.3",
|
||||||
|
"@ljharb/eslint-config": "^21.1.1",
|
||||||
|
"@ljharb/tsconfig": "^0.2.3",
|
||||||
|
"@types/for-each": "^0.3.3",
|
||||||
|
"@types/function-bind": "^1.1.10",
|
||||||
|
"@types/object-inspect": "^1.13.0",
|
||||||
|
"@types/tape": "^5.8.1",
|
||||||
|
"auto-changelog": "^2.5.0",
|
||||||
|
"encoding": "^0.1.13",
|
||||||
|
"es-value-fixtures": "^1.7.1",
|
||||||
|
"eslint": "=8.8.0",
|
||||||
|
"evalmd": "^0.0.19",
|
||||||
|
"for-each": "^0.3.5",
|
||||||
|
"has-strict-mode": "^1.1.0",
|
||||||
|
"in-publish": "^2.0.1",
|
||||||
|
"npmignore": "^0.3.1",
|
||||||
|
"nyc": "^10.3.2",
|
||||||
|
"object-inspect": "^1.13.4",
|
||||||
|
"safe-publish-latest": "^2.0.0",
|
||||||
|
"tape": "^5.9.0",
|
||||||
|
"typescript": "next"
|
||||||
|
},
|
||||||
|
"testling": {
|
||||||
|
"files": "test/index.js"
|
||||||
|
},
|
||||||
|
"auto-changelog": {
|
||||||
|
"output": "CHANGELOG.md",
|
||||||
|
"template": "keepachangelog",
|
||||||
|
"unreleased": false,
|
||||||
|
"commitLimit": false,
|
||||||
|
"backfillLimit": false,
|
||||||
|
"hideCredit": true
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"ignore": [
|
||||||
|
".github/workflows"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
declare const reflectApply: false | typeof Reflect.apply;
|
||||||
|
|
||||||
|
export = reflectApply;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('./reflectApply')} */
|
||||||
|
module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply;
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var callBind = require('../');
|
||||||
|
var hasStrictMode = require('has-strict-mode')();
|
||||||
|
var forEach = require('for-each');
|
||||||
|
var inspect = require('object-inspect');
|
||||||
|
var v = require('es-value-fixtures');
|
||||||
|
|
||||||
|
var test = require('tape');
|
||||||
|
|
||||||
|
test('callBindBasic', function (t) {
|
||||||
|
forEach(v.nonFunctions, function (nonFunction) {
|
||||||
|
t['throws'](
|
||||||
|
// @ts-expect-error
|
||||||
|
function () { callBind([nonFunction]); },
|
||||||
|
TypeError,
|
||||||
|
inspect(nonFunction) + ' is not a function'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
var sentinel = { sentinel: true };
|
||||||
|
/** @type {<T, A extends number, B extends number>(this: T, a: A, b: B) => [T | undefined, A, B]} */
|
||||||
|
var func = function (a, b) {
|
||||||
|
// eslint-disable-next-line no-invalid-this
|
||||||
|
return [!hasStrictMode && this === global ? undefined : this, a, b];
|
||||||
|
};
|
||||||
|
t.equal(func.length, 2, 'original function length is 2');
|
||||||
|
|
||||||
|
/** type {(thisArg: unknown, a: number, b: number) => [unknown, number, number]} */
|
||||||
|
var bound = callBind([func]);
|
||||||
|
/** type {((a: number, b: number) => [typeof sentinel, typeof a, typeof b])} */
|
||||||
|
var boundR = callBind([func, sentinel]);
|
||||||
|
/** type {((b: number) => [typeof sentinel, number, typeof b])} */
|
||||||
|
var boundArg = callBind([func, sentinel, /** @type {const} */ (1)]);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with no args');
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(bound(1, 2), [hasStrictMode ? 1 : Object(1), 2, undefined], 'bound func too few args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args');
|
||||||
|
|
||||||
|
t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args');
|
||||||
|
t.deepEqual(bound(1, 2, 3), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with right args');
|
||||||
|
t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args');
|
||||||
|
t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg');
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(bound(1, 2, 3, 4), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with too many args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args');
|
||||||
|
// @ts-expect-error
|
||||||
|
t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "@ljharb/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2021",
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"coverage",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
|
||||||
|
"extends": "@ljharb",
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"new-cap": [2, {
|
||||||
|
"capIsNewExceptions": [
|
||||||
|
"GetIntrinsic",
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [ljharb]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: npm/call-bound
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"all": true,
|
||||||
|
"check-coverage": false,
|
||||||
|
"reporter": ["text-summary", "text", "html", "json"],
|
||||||
|
"exclude": [
|
||||||
|
"coverage",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.0.4](https://github.com/ljharb/call-bound/compare/v1.0.3...v1.0.4) - 2025-03-03
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- [types] improve types [`e648922`](https://github.com/ljharb/call-bound/commit/e6489222a9e54f350fbf952ceabe51fd8b6027ff)
|
||||||
|
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`a42a5eb`](https://github.com/ljharb/call-bound/commit/a42a5ebe6c1b54fcdc7997c7dc64fdca9e936719)
|
||||||
|
- [Deps] update `call-bind-apply-helpers`, `get-intrinsic` [`f529eac`](https://github.com/ljharb/call-bound/commit/f529eac132404c17156bbc23ab2297a25d0f20b8)
|
||||||
|
|
||||||
|
## [v1.0.3](https://github.com/ljharb/call-bound/compare/v1.0.2...v1.0.3) - 2024-12-15
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- [Refactor] use `call-bind-apply-helpers` instead of `call-bind` [`5e0b134`](https://github.com/ljharb/call-bound/commit/5e0b13496df14fb7d05dae9412f088da8d3f75be)
|
||||||
|
- [Deps] update `get-intrinsic` [`41fc967`](https://github.com/ljharb/call-bound/commit/41fc96732a22c7b7e8f381f93ccc54bb6293be2e)
|
||||||
|
- [readme] fix example [`79a0137`](https://github.com/ljharb/call-bound/commit/79a0137723f7c6d09c9c05452bbf8d5efb5d6e49)
|
||||||
|
- [meta] add `sideEffects` flag [`08b07be`](https://github.com/ljharb/call-bound/commit/08b07be7f1c03f67dc6f3cdaf0906259771859f7)
|
||||||
|
|
||||||
|
## [v1.0.2](https://github.com/ljharb/call-bound/compare/v1.0.1...v1.0.2) - 2024-12-10
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `gopd` [`e6a5ffe`](https://github.com/ljharb/call-bound/commit/e6a5ffe849368fe4f74dfd6cdeca1b9baa39e8d5)
|
||||||
|
- [Deps] update `call-bind`, `get-intrinsic` [`2aeb5b5`](https://github.com/ljharb/call-bound/commit/2aeb5b521dc2b2683d1345c753ea1161de2d1c14)
|
||||||
|
- [types] improve return type [`1a0c9fe`](https://github.com/ljharb/call-bound/commit/1a0c9fe3114471e7ca1f57d104e2efe713bb4871)
|
||||||
|
|
||||||
|
## v1.0.1 - 2024-12-05
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- Initial implementation, tests, readme, types [`6d94121`](https://github.com/ljharb/call-bound/commit/6d94121a9243602e506334069f7a03189fe3363d)
|
||||||
|
- Initial commit [`0eae867`](https://github.com/ljharb/call-bound/commit/0eae867334ea025c33e6e91cdecfc9df96680cf9)
|
||||||
|
- npm init [`71b2479`](https://github.com/ljharb/call-bound/commit/71b2479c6723e0b7d91a6b663613067e98b7b275)
|
||||||
|
- Only apps should have lockfiles [`c3754a9`](https://github.com/ljharb/call-bound/commit/c3754a949b7f9132b47e2d18c1729889736741eb)
|
||||||
|
- [actions] skip `npm ls` in node < 10 [`74275a5`](https://github.com/ljharb/call-bound/commit/74275a5186b8caf6309b6b97472bdcb0df4683a8)
|
||||||
|
- [Dev Deps] add missing peer dep [`1354de8`](https://github.com/ljharb/call-bound/commit/1354de8679413e4ae9c523d85f76fa7a5e032d97)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Jordan Harband
|
||||||
|
|
||||||
|
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,53 @@
|
||||||
|
# call-bound <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
|
||||||
|
|
||||||
|
[![github actions][actions-image]][actions-url]
|
||||||
|
[![coverage][codecov-image]][codecov-url]
|
||||||
|
[![dependency status][deps-svg]][deps-url]
|
||||||
|
[![dev dependency status][dev-deps-svg]][dev-deps-url]
|
||||||
|
[![License][license-image]][license-url]
|
||||||
|
[![Downloads][downloads-image]][downloads-url]
|
||||||
|
|
||||||
|
[![npm badge][npm-badge-png]][package-url]
|
||||||
|
|
||||||
|
Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install --save call-bound
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage/Examples
|
||||||
|
|
||||||
|
```js
|
||||||
|
const assert = require('assert');
|
||||||
|
const callBound = require('call-bound');
|
||||||
|
|
||||||
|
const slice = callBound('Array.prototype.slice');
|
||||||
|
|
||||||
|
delete Function.prototype.call;
|
||||||
|
delete Function.prototype.bind;
|
||||||
|
delete Array.prototype.slice;
|
||||||
|
|
||||||
|
assert.deepEqual(slice([1, 2, 3, 4], 1, -1), [2, 3]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Clone the repo, `npm install`, and run `npm test`
|
||||||
|
|
||||||
|
[package-url]: https://npmjs.org/package/call-bound
|
||||||
|
[npm-version-svg]: https://versionbadg.es/ljharb/call-bound.svg
|
||||||
|
[deps-svg]: https://david-dm.org/ljharb/call-bound.svg
|
||||||
|
[deps-url]: https://david-dm.org/ljharb/call-bound
|
||||||
|
[dev-deps-svg]: https://david-dm.org/ljharb/call-bound/dev-status.svg
|
||||||
|
[dev-deps-url]: https://david-dm.org/ljharb/call-bound#info=devDependencies
|
||||||
|
[npm-badge-png]: https://nodei.co/npm/call-bound.png?downloads=true&stars=true
|
||||||
|
[license-image]: https://img.shields.io/npm/l/call-bound.svg
|
||||||
|
[license-url]: LICENSE
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/call-bound.svg
|
||||||
|
[downloads-url]: https://npm-stat.com/charts.html?package=call-bound
|
||||||
|
[codecov-image]: https://codecov.io/gh/ljharb/call-bound/branch/main/graphs/badge.svg
|
||||||
|
[codecov-url]: https://app.codecov.io/gh/ljharb/call-bound/
|
||||||
|
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bound
|
||||||
|
[actions-url]: https://github.com/ljharb/call-bound/actions
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
type Intrinsic = typeof globalThis;
|
||||||
|
|
||||||
|
type IntrinsicName = keyof Intrinsic | `%${keyof Intrinsic}%`;
|
||||||
|
|
||||||
|
type IntrinsicPath = IntrinsicName | `${StripPercents<IntrinsicName>}.${string}` | `%${StripPercents<IntrinsicName>}.${string}%`;
|
||||||
|
|
||||||
|
type AllowMissing = boolean;
|
||||||
|
|
||||||
|
type StripPercents<T extends string> = T extends `%${infer U}%` ? U : T;
|
||||||
|
|
||||||
|
type BindMethodPrecise<F> =
|
||||||
|
F extends (this: infer This, ...args: infer Args) => infer R
|
||||||
|
? (obj: This, ...args: Args) => R
|
||||||
|
: F extends {
|
||||||
|
(this: infer This1, ...args: infer Args1): infer R1;
|
||||||
|
(this: infer This2, ...args: infer Args2): infer R2
|
||||||
|
}
|
||||||
|
? {
|
||||||
|
(obj: This1, ...args: Args1): R1;
|
||||||
|
(obj: This2, ...args: Args2): R2
|
||||||
|
}
|
||||||
|
: never
|
||||||
|
|
||||||
|
// Extract method type from a prototype
|
||||||
|
type GetPrototypeMethod<T extends keyof typeof globalThis, M extends string> =
|
||||||
|
(typeof globalThis)[T] extends { prototype: any }
|
||||||
|
? M extends keyof (typeof globalThis)[T]['prototype']
|
||||||
|
? (typeof globalThis)[T]['prototype'][M]
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
// Get static property/method
|
||||||
|
type GetStaticMember<T extends keyof typeof globalThis, P extends string> =
|
||||||
|
P extends keyof (typeof globalThis)[T] ? (typeof globalThis)[T][P] : never
|
||||||
|
|
||||||
|
// Type that maps string path to actual bound function or value with better precision
|
||||||
|
type BoundIntrinsic<S extends string> =
|
||||||
|
S extends `${infer Obj}.prototype.${infer Method}`
|
||||||
|
? Obj extends keyof typeof globalThis
|
||||||
|
? BindMethodPrecise<GetPrototypeMethod<Obj, Method & string>>
|
||||||
|
: unknown
|
||||||
|
: S extends `${infer Obj}.${infer Prop}`
|
||||||
|
? Obj extends keyof typeof globalThis
|
||||||
|
? GetStaticMember<Obj, Prop & string>
|
||||||
|
: unknown
|
||||||
|
: unknown
|
||||||
|
|
||||||
|
declare function arraySlice<T>(array: readonly T[], start?: number, end?: number): T[];
|
||||||
|
declare function arraySlice<T>(array: ArrayLike<T>, start?: number, end?: number): T[];
|
||||||
|
declare function arraySlice<T>(array: IArguments, start?: number, end?: number): T[];
|
||||||
|
|
||||||
|
// Special cases for methods that need explicit typing
|
||||||
|
interface SpecialCases {
|
||||||
|
'%Object.prototype.isPrototypeOf%': (thisArg: {}, obj: unknown) => boolean;
|
||||||
|
'%String.prototype.replace%': {
|
||||||
|
(str: string, searchValue: string | RegExp, replaceValue: string): string;
|
||||||
|
(str: string, searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string
|
||||||
|
};
|
||||||
|
'%Object.prototype.toString%': (obj: {}) => string;
|
||||||
|
'%Object.prototype.hasOwnProperty%': (obj: {}, v: PropertyKey) => boolean;
|
||||||
|
'%Array.prototype.slice%': typeof arraySlice;
|
||||||
|
'%Array.prototype.map%': <T, U>(array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any) => U[];
|
||||||
|
'%Array.prototype.filter%': <T>(array: readonly T[], predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any) => T[];
|
||||||
|
'%Array.prototype.indexOf%': <T>(array: readonly T[], searchElement: T, fromIndex?: number) => number;
|
||||||
|
'%Function.prototype.apply%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, args: A) => R;
|
||||||
|
'%Function.prototype.call%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => R;
|
||||||
|
'%Function.prototype.bind%': <T, A extends any[], R>(fn: (...args: A) => R, thisArg: any, ...args: A) => (...remainingArgs: A) => R;
|
||||||
|
'%Promise.prototype.then%': {
|
||||||
|
<T, R>(promise: Promise<T>, onfulfilled: (value: T) => R | PromiseLike<R>): Promise<R>;
|
||||||
|
<T, R>(promise: Promise<T>, onfulfilled: ((value: T) => R | PromiseLike<R>) | undefined | null, onrejected: (reason: any) => R | PromiseLike<R>): Promise<R>;
|
||||||
|
};
|
||||||
|
'%RegExp.prototype.test%': (regexp: RegExp, str: string) => boolean;
|
||||||
|
'%RegExp.prototype.exec%': (regexp: RegExp, str: string) => RegExpExecArray | null;
|
||||||
|
'%Error.prototype.toString%': (error: Error) => string;
|
||||||
|
'%TypeError.prototype.toString%': (error: TypeError) => string;
|
||||||
|
'%String.prototype.split%': (
|
||||||
|
obj: unknown,
|
||||||
|
splitter: string | RegExp | {
|
||||||
|
[Symbol.split](string: string, limit?: number): string[];
|
||||||
|
},
|
||||||
|
limit?: number | undefined
|
||||||
|
) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a bound function for a prototype method, or a value for a static property.
|
||||||
|
*
|
||||||
|
* @param name - The name of the intrinsic (e.g. 'Array.prototype.slice')
|
||||||
|
* @param {AllowMissing} [allowMissing] - Whether to allow missing intrinsics (default: false)
|
||||||
|
*/
|
||||||
|
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: K, allowMissing?: AllowMissing): SpecialCases[`%${StripPercents<K>}%`];
|
||||||
|
declare function callBound<K extends keyof SpecialCases | StripPercents<keyof SpecialCases>, S extends IntrinsicPath>(name: S, allowMissing?: AllowMissing): BoundIntrinsic<S>;
|
||||||
|
|
||||||
|
export = callBound;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var GetIntrinsic = require('get-intrinsic');
|
||||||
|
|
||||||
|
var callBindBasic = require('call-bind-apply-helpers');
|
||||||
|
|
||||||
|
/** @type {(thisArg: string, searchString: string, position?: number) => number} */
|
||||||
|
var $indexOf = callBindBasic([GetIntrinsic('%String.prototype.indexOf%')]);
|
||||||
|
|
||||||
|
/** @type {import('.')} */
|
||||||
|
module.exports = function callBoundIntrinsic(name, allowMissing) {
|
||||||
|
/* eslint no-extra-parens: 0 */
|
||||||
|
|
||||||
|
var intrinsic = /** @type {(this: unknown, ...args: unknown[]) => unknown} */ (GetIntrinsic(name, !!allowMissing));
|
||||||
|
if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {
|
||||||
|
return callBindBasic(/** @type {const} */ ([intrinsic]));
|
||||||
|
}
|
||||||
|
return intrinsic;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"name": "call-bound",
|
||||||
|
"version": "1.0.4",
|
||||||
|
"description": "Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.",
|
||||||
|
"main": "index.js",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"prepack": "npmignore --auto --commentLines=auto",
|
||||||
|
"prepublish": "not-in-publish || npm run prepublishOnly",
|
||||||
|
"prepublishOnly": "safe-publish-latest",
|
||||||
|
"prelint": "evalmd README.md",
|
||||||
|
"lint": "eslint --ext=.js,.mjs .",
|
||||||
|
"postlint": "tsc -p . && attw -P",
|
||||||
|
"pretest": "npm run lint",
|
||||||
|
"tests-only": "nyc tape 'test/**/*.js'",
|
||||||
|
"test": "npm run tests-only",
|
||||||
|
"posttest": "npx npm@'>=10.2' audit --production",
|
||||||
|
"version": "auto-changelog && git add CHANGELOG.md",
|
||||||
|
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ljharb/call-bound.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"javascript",
|
||||||
|
"ecmascript",
|
||||||
|
"es",
|
||||||
|
"js",
|
||||||
|
"callbind",
|
||||||
|
"callbound",
|
||||||
|
"call",
|
||||||
|
"bind",
|
||||||
|
"bound",
|
||||||
|
"call-bind",
|
||||||
|
"call-bound",
|
||||||
|
"function",
|
||||||
|
"es-abstract"
|
||||||
|
],
|
||||||
|
"author": "Jordan Harband <ljharb@gmail.com>",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ljharb/call-bound/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/ljharb/call-bound#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@arethetypeswrong/cli": "^0.17.4",
|
||||||
|
"@ljharb/eslint-config": "^21.1.1",
|
||||||
|
"@ljharb/tsconfig": "^0.3.0",
|
||||||
|
"@types/call-bind": "^1.0.5",
|
||||||
|
"@types/get-intrinsic": "^1.2.3",
|
||||||
|
"@types/tape": "^5.8.1",
|
||||||
|
"auto-changelog": "^2.5.0",
|
||||||
|
"encoding": "^0.1.13",
|
||||||
|
"es-value-fixtures": "^1.7.1",
|
||||||
|
"eslint": "=8.8.0",
|
||||||
|
"evalmd": "^0.0.19",
|
||||||
|
"for-each": "^0.3.5",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-strict-mode": "^1.1.0",
|
||||||
|
"in-publish": "^2.0.1",
|
||||||
|
"npmignore": "^0.3.1",
|
||||||
|
"nyc": "^10.3.2",
|
||||||
|
"object-inspect": "^1.13.4",
|
||||||
|
"safe-publish-latest": "^2.0.0",
|
||||||
|
"tape": "^5.9.0",
|
||||||
|
"typescript": "next"
|
||||||
|
},
|
||||||
|
"testling": {
|
||||||
|
"files": "test/index.js"
|
||||||
|
},
|
||||||
|
"auto-changelog": {
|
||||||
|
"output": "CHANGELOG.md",
|
||||||
|
"template": "keepachangelog",
|
||||||
|
"unreleased": false,
|
||||||
|
"commitLimit": false,
|
||||||
|
"backfillLimit": false,
|
||||||
|
"hideCredit": true
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"ignore": [
|
||||||
|
".github/workflows"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var test = require('tape');
|
||||||
|
|
||||||
|
var callBound = require('../');
|
||||||
|
|
||||||
|
/** @template {true} T @template U @typedef {T extends U ? T : never} AssertType */
|
||||||
|
|
||||||
|
test('callBound', function (t) {
|
||||||
|
// static primitive
|
||||||
|
t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself');
|
||||||
|
t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself');
|
||||||
|
|
||||||
|
// static non-function object
|
||||||
|
t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself');
|
||||||
|
t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself');
|
||||||
|
t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself');
|
||||||
|
t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself');
|
||||||
|
|
||||||
|
// static function
|
||||||
|
t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself');
|
||||||
|
t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself');
|
||||||
|
|
||||||
|
// prototype primitive
|
||||||
|
t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself');
|
||||||
|
t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself');
|
||||||
|
|
||||||
|
var x = callBound('Object.prototype.toString');
|
||||||
|
var y = callBound('%Object.prototype.toString%');
|
||||||
|
|
||||||
|
// prototype function
|
||||||
|
t.notEqual(x, Object.prototype.toString, 'Object.prototype.toString does not yield itself');
|
||||||
|
t.notEqual(y, Object.prototype.toString, '%Object.prototype.toString% does not yield itself');
|
||||||
|
t.equal(x(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original');
|
||||||
|
t.equal(y(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original');
|
||||||
|
|
||||||
|
t['throws'](
|
||||||
|
// @ts-expect-error
|
||||||
|
function () { callBound('does not exist'); },
|
||||||
|
SyntaxError,
|
||||||
|
'nonexistent intrinsic throws'
|
||||||
|
);
|
||||||
|
t['throws'](
|
||||||
|
// @ts-expect-error
|
||||||
|
function () { callBound('does not exist', true); },
|
||||||
|
SyntaxError,
|
||||||
|
'allowMissing arg still throws for unknown intrinsic'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) {
|
||||||
|
st['throws'](
|
||||||
|
function () { callBound('WeakRef'); },
|
||||||
|
TypeError,
|
||||||
|
'real but absent intrinsic throws'
|
||||||
|
);
|
||||||
|
st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception');
|
||||||
|
st.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "@ljharb/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["es2024"],
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"coverage",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2017 Douglas Christopher Wilson
|
||||||
|
|
||||||
|
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,141 @@
|
||||||
|
# content-disposition
|
||||||
|
|
||||||
|
[![NPM Version][npm-image]][npm-url]
|
||||||
|
[![NPM Downloads][downloads-image]][downloads-url]
|
||||||
|
[![Node.js Version][node-version-image]][node-version-url]
|
||||||
|
[![Build Status][github-actions-ci-image]][github-actions-ci-url]
|
||||||
|
[![Test Coverage][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Create and parse HTTP `Content-Disposition` header
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install content-disposition
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
const contentDisposition = require('content-disposition')
|
||||||
|
```
|
||||||
|
|
||||||
|
### contentDisposition(filename, options)
|
||||||
|
|
||||||
|
Create an attachment `Content-Disposition` header value using the given file name,
|
||||||
|
if supplied. The `filename` is optional and if no file name is desired, but you
|
||||||
|
want to specify `options`, set `filename` to `undefined`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf'))
|
||||||
|
```
|
||||||
|
|
||||||
|
**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this
|
||||||
|
header through a means different from `setHeader` in Node.js, you'll want to specify
|
||||||
|
the `'binary'` encoding in Node.js.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
`contentDisposition` accepts these properties in the options object.
|
||||||
|
|
||||||
|
##### fallback
|
||||||
|
|
||||||
|
If the `filename` option is outside ISO-8859-1, then the file name is actually
|
||||||
|
stored in a supplemental field for clients that support Unicode file names and
|
||||||
|
a ISO-8859-1 version of the file name is automatically generated.
|
||||||
|
|
||||||
|
This specifies the ISO-8859-1 file name to override the automatic generation or
|
||||||
|
disables the generation all together, defaults to `true`.
|
||||||
|
|
||||||
|
- A string will specify the ISO-8859-1 file name to use in place of automatic
|
||||||
|
generation.
|
||||||
|
- `false` will disable including a ISO-8859-1 file name and only include the
|
||||||
|
Unicode version (unless the file name is already ISO-8859-1).
|
||||||
|
- `true` will enable automatic generation if the file name is outside ISO-8859-1.
|
||||||
|
|
||||||
|
If the `filename` option is ISO-8859-1 and this option is specified and has a
|
||||||
|
different value, then the `filename` option is encoded in the extended field
|
||||||
|
and this set as the fallback field, even though they are both ISO-8859-1.
|
||||||
|
|
||||||
|
##### type
|
||||||
|
|
||||||
|
Specifies the disposition type, defaults to `"attachment"`. This can also be
|
||||||
|
`"inline"`, or any other value (all values except inline are treated like
|
||||||
|
`attachment`, but can convey additional information if both parties agree to
|
||||||
|
it). The type is normalized to lower-case.
|
||||||
|
|
||||||
|
### contentDisposition.parse(string)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt')
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse a `Content-Disposition` header string. This automatically handles extended
|
||||||
|
("Unicode") parameters by decoding them and providing them under the standard
|
||||||
|
parameter name. This will return an object with the following properties (examples
|
||||||
|
are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`):
|
||||||
|
|
||||||
|
- `type`: The disposition type (always lower case). Example: `'attachment'`
|
||||||
|
|
||||||
|
- `parameters`: An object of the parameters in the disposition (name of parameter
|
||||||
|
always lower case and extended versions replace non-extended versions). Example:
|
||||||
|
`{filename: "€ rates.txt"}`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Send a file for download
|
||||||
|
|
||||||
|
```js
|
||||||
|
const contentDisposition = require('content-disposition')
|
||||||
|
const fs = require('fs')
|
||||||
|
const http = require('http')
|
||||||
|
const onFinished = require('on-finished')
|
||||||
|
|
||||||
|
const filePath = '/path/to/public/plans.pdf'
|
||||||
|
|
||||||
|
http.createServer(function onRequest (req, res) {
|
||||||
|
// set headers
|
||||||
|
res.setHeader('Content-Type', 'application/pdf')
|
||||||
|
res.setHeader('Content-Disposition', contentDisposition(filePath))
|
||||||
|
|
||||||
|
// send file
|
||||||
|
const stream = fs.createReadStream(filePath)
|
||||||
|
stream.pipe(res)
|
||||||
|
onFinished(res, function () {
|
||||||
|
stream.destroy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616]
|
||||||
|
- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987]
|
||||||
|
- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266]
|
||||||
|
- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231]
|
||||||
|
|
||||||
|
[rfc-2616]: https://datatracker.ietf.org/doc/html/rfc2616
|
||||||
|
[rfc-5987]: https://datatracker.ietf.org/doc/html/rfc5987
|
||||||
|
[rfc-6266]: https://datatracker.ietf.org/doc/html/rfc6266
|
||||||
|
[tc-2231]: http://greenbytes.de/tech/tc2231/
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[npm-image]: https://img.shields.io/npm/v/content-disposition
|
||||||
|
[npm-url]: https://www.npmjs.com/package/content-disposition
|
||||||
|
[node-version-image]: https://img.shields.io/node/v/content-disposition
|
||||||
|
[node-version-url]: https://nodejs.org/en/download
|
||||||
|
[coveralls-image]: https://img.shields.io/coverallsCoverage/github/jshttp/content-disposition
|
||||||
|
[coveralls-url]: https://coveralls.io/github/jshttp/content-disposition?branch=master
|
||||||
|
[downloads-image]: https://img.shields.io/npm/dm/content-disposition
|
||||||
|
[downloads-url]: https://www.npmjs.com/package/content-disposition
|
||||||
|
[github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/jshttp/content-disposition/ci.yml
|
||||||
|
[github-actions-ci-url]: https://github.com/jshttp/content-disposition/actions/workflows/ci.yml
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
/*!
|
||||||
|
* content-disposition
|
||||||
|
* Copyright(c) 2014-2017 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = contentDisposition
|
||||||
|
module.exports.parse = parse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDecoder instance for UTF-8 decoding when decodeURIComponent fails due to invalid byte sequences.
|
||||||
|
* @type {TextDecoder}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const utf8Decoder = new TextDecoder('utf-8')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%")
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match non-latin1 characters.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match quoted-pair in RFC 2616
|
||||||
|
*
|
||||||
|
* quoted-pair = "\" CHAR
|
||||||
|
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match chars that must be quoted-pair in RFC 2616
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var QUOTE_REGEXP = /([\\"])/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp for various RFC 2616 grammar
|
||||||
|
*
|
||||||
|
* parameter = token "=" ( token | quoted-string )
|
||||||
|
* token = 1*<any CHAR except CTLs or separators>
|
||||||
|
* separators = "(" | ")" | "<" | ">" | "@"
|
||||||
|
* | "," | ";" | ":" | "\" | <">
|
||||||
|
* | "/" | "[" | "]" | "?" | "="
|
||||||
|
* | "{" | "}" | SP | HT
|
||||||
|
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
||||||
|
* qdtext = <any TEXT except <">>
|
||||||
|
* quoted-pair = "\" CHAR
|
||||||
|
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||||
|
* TEXT = <any OCTET except CTLs, but including LWS>
|
||||||
|
* LWS = [CRLF] 1*( SP | HT )
|
||||||
|
* CRLF = CR LF
|
||||||
|
* CR = <US-ASCII CR, carriage return (13)>
|
||||||
|
* LF = <US-ASCII LF, linefeed (10)>
|
||||||
|
* SP = <US-ASCII SP, space (32)>
|
||||||
|
* HT = <US-ASCII HT, horizontal-tab (9)>
|
||||||
|
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||||
|
* OCTET = <any 8-bit sequence of data>
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex
|
||||||
|
var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/
|
||||||
|
var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp for various RFC 5987 grammar
|
||||||
|
*
|
||||||
|
* ext-value = charset "'" [ language ] "'" value-chars
|
||||||
|
* charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
||||||
|
* mime-charset = 1*mime-charsetc
|
||||||
|
* mime-charsetc = ALPHA / DIGIT
|
||||||
|
* / "!" / "#" / "$" / "%" / "&"
|
||||||
|
* / "+" / "-" / "^" / "_" / "`"
|
||||||
|
* / "{" / "}" / "~"
|
||||||
|
* language = ( 2*3ALPHA [ extlang ] )
|
||||||
|
* / 4ALPHA
|
||||||
|
* / 5*8ALPHA
|
||||||
|
* extlang = *3( "-" 3ALPHA )
|
||||||
|
* value-chars = *( pct-encoded / attr-char )
|
||||||
|
* pct-encoded = "%" HEXDIG HEXDIG
|
||||||
|
* attr-char = ALPHA / DIGIT
|
||||||
|
* / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
||||||
|
* / "^" / "_" / "`" / "|" / "~"
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp for various RFC 6266 grammar
|
||||||
|
*
|
||||||
|
* disposition-type = "inline" | "attachment" | disp-ext-type
|
||||||
|
* disp-ext-type = token
|
||||||
|
* disposition-parm = filename-parm | disp-ext-parm
|
||||||
|
* filename-parm = "filename" "=" value
|
||||||
|
* | "filename*" "=" ext-value
|
||||||
|
* disp-ext-parm = token "=" value
|
||||||
|
* | ext-token "=" ext-value
|
||||||
|
* ext-token = <the characters in token, followed by "*">
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an attachment Content-Disposition header.
|
||||||
|
*
|
||||||
|
* @param {string} [filename]
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {string} [options.type=attachment]
|
||||||
|
* @param {string|boolean} [options.fallback=true]
|
||||||
|
* @return {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function contentDisposition (filename, options) {
|
||||||
|
var opts = options || {}
|
||||||
|
|
||||||
|
// get type
|
||||||
|
var type = opts.type || 'attachment'
|
||||||
|
|
||||||
|
// get parameters
|
||||||
|
var params = createparams(filename, opts.fallback)
|
||||||
|
|
||||||
|
// format into string
|
||||||
|
return format(new ContentDisposition(type, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create parameters object from filename and fallback.
|
||||||
|
*
|
||||||
|
* @param {string} [filename]
|
||||||
|
* @param {string|boolean} [fallback=true]
|
||||||
|
* @return {object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createparams (filename, fallback) {
|
||||||
|
if (filename === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = {}
|
||||||
|
|
||||||
|
if (typeof filename !== 'string') {
|
||||||
|
throw new TypeError('filename must be a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback defaults to true
|
||||||
|
if (fallback === undefined) {
|
||||||
|
fallback = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof fallback !== 'string' && typeof fallback !== 'boolean') {
|
||||||
|
throw new TypeError('fallback must be a string or boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) {
|
||||||
|
throw new TypeError('fallback must be ISO-8859-1 string')
|
||||||
|
}
|
||||||
|
|
||||||
|
// restrict to file base name
|
||||||
|
var name = basename(filename)
|
||||||
|
|
||||||
|
// determine if name is suitable for quoted string
|
||||||
|
var isQuotedString = TEXT_REGEXP.test(name)
|
||||||
|
|
||||||
|
// generate fallback name
|
||||||
|
var fallbackName = typeof fallback !== 'string'
|
||||||
|
? fallback && getlatin1(name)
|
||||||
|
: basename(fallback)
|
||||||
|
var hasFallback = typeof fallbackName === 'string' && fallbackName !== name
|
||||||
|
|
||||||
|
// set extended filename parameter
|
||||||
|
if (hasFallback || !isQuotedString || hasHexEscape(name)) {
|
||||||
|
params['filename*'] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// set filename parameter
|
||||||
|
if (isQuotedString || hasFallback) {
|
||||||
|
params.filename = hasFallback
|
||||||
|
? fallbackName
|
||||||
|
: name
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format object to Content-Disposition header.
|
||||||
|
*
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} obj.type
|
||||||
|
* @param {object} [obj.parameters]
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function format (obj) {
|
||||||
|
var parameters = obj.parameters
|
||||||
|
var type = obj.type
|
||||||
|
|
||||||
|
if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) {
|
||||||
|
throw new TypeError('invalid type')
|
||||||
|
}
|
||||||
|
|
||||||
|
// start with normalized type
|
||||||
|
var string = String(type).toLowerCase()
|
||||||
|
|
||||||
|
// append parameters
|
||||||
|
if (parameters && typeof parameters === 'object') {
|
||||||
|
var param
|
||||||
|
var params = Object.keys(parameters).sort()
|
||||||
|
|
||||||
|
for (var i = 0; i < params.length; i++) {
|
||||||
|
param = params[i]
|
||||||
|
|
||||||
|
var val = param.slice(-1) === '*'
|
||||||
|
? ustring(parameters[param])
|
||||||
|
: qstring(parameters[param])
|
||||||
|
|
||||||
|
string += '; ' + param + '=' + val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a RFC 5987 field value (gracefully).
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function decodefield (str) {
|
||||||
|
const match = EXT_VALUE_REGEXP.exec(str)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new TypeError('invalid extended field value')
|
||||||
|
}
|
||||||
|
|
||||||
|
const charset = match[1].toLowerCase()
|
||||||
|
const encoded = match[2]
|
||||||
|
|
||||||
|
switch (charset) {
|
||||||
|
case 'iso-8859-1':
|
||||||
|
{
|
||||||
|
const binary = decodeHexEscapes(encoded)
|
||||||
|
return getlatin1(binary)
|
||||||
|
}
|
||||||
|
case 'utf-8':
|
||||||
|
case 'utf8':
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(encoded)
|
||||||
|
} catch {
|
||||||
|
// Failed to decode with decodeURIComponent, fallback to lenient decoding which replaces invalid UTF-8 byte sequences with the Unicode replacement character
|
||||||
|
// TODO: Consider removing in the next major version to be more strict about invalid percent-encodings
|
||||||
|
const binary = decodeHexEscapes(encoded)
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(binary.length)
|
||||||
|
for (let idx = 0; idx < binary.length; idx++) {
|
||||||
|
bytes[idx] = binary.charCodeAt(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utf8Decoder.decode(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new TypeError('unsupported charset in extended field')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ISO-8859-1 version of string.
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getlatin1 (val) {
|
||||||
|
// simple Unicode -> ISO-8859-1 transformation
|
||||||
|
return String(val).replace(NON_LATIN1_REGEXP, '?')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Content-Disposition header string.
|
||||||
|
*
|
||||||
|
* @param {string} string
|
||||||
|
* @return {object}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function parse (string) {
|
||||||
|
if (!string || typeof string !== 'string') {
|
||||||
|
throw new TypeError('argument string is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = DISPOSITION_TYPE_REGEXP.exec(string)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new TypeError('invalid type format')
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize type
|
||||||
|
var index = match[0].length
|
||||||
|
var type = match[1].toLowerCase()
|
||||||
|
|
||||||
|
var key
|
||||||
|
var names = []
|
||||||
|
var params = {}
|
||||||
|
var value
|
||||||
|
|
||||||
|
// calculate index to start at
|
||||||
|
index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';'
|
||||||
|
? index - 1
|
||||||
|
: index
|
||||||
|
|
||||||
|
// match parameters
|
||||||
|
while ((match = PARAM_REGEXP.exec(string))) {
|
||||||
|
if (match.index !== index) {
|
||||||
|
throw new TypeError('invalid parameter format')
|
||||||
|
}
|
||||||
|
|
||||||
|
index += match[0].length
|
||||||
|
key = match[1].toLowerCase()
|
||||||
|
value = match[2]
|
||||||
|
|
||||||
|
if (names.indexOf(key) !== -1) {
|
||||||
|
throw new TypeError('invalid duplicate parameter')
|
||||||
|
}
|
||||||
|
|
||||||
|
names.push(key)
|
||||||
|
|
||||||
|
if (key.indexOf('*') + 1 === key.length) {
|
||||||
|
// decode extended value
|
||||||
|
key = key.slice(0, -1)
|
||||||
|
value = decodefield(value)
|
||||||
|
|
||||||
|
// overwrite existing value
|
||||||
|
params[key] = value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params[key] === 'string') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] === '"') {
|
||||||
|
// remove quotes and escapes
|
||||||
|
value = value
|
||||||
|
.slice(1, -1)
|
||||||
|
.replace(QESC_REGEXP, '$1')
|
||||||
|
}
|
||||||
|
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index !== -1 && index !== string.length) {
|
||||||
|
throw new TypeError('invalid parameter format')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ContentDisposition(type, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percent encode a single character.
|
||||||
|
*
|
||||||
|
* @param {string} char
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function pencode (char) {
|
||||||
|
return '%' + String(char)
|
||||||
|
.charCodeAt(0)
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote a string for HTTP.
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function qstring (val) {
|
||||||
|
var str = String(val)
|
||||||
|
|
||||||
|
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a Unicode string for HTTP (RFC 5987).
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ustring (val) {
|
||||||
|
var str = String(val)
|
||||||
|
|
||||||
|
// percent encode as UTF-8
|
||||||
|
var encoded = encodeURIComponent(str)
|
||||||
|
.replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode)
|
||||||
|
|
||||||
|
return 'UTF-8\'\'' + encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for parsed Content-Disposition header for v8 optimization
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param {string} type
|
||||||
|
* @param {object} parameters
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ContentDisposition (type, parameters) {
|
||||||
|
this.type = type
|
||||||
|
this.parameters = parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the last portion of a path
|
||||||
|
*
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function basename (path) {
|
||||||
|
const normalized = path.replaceAll('\\', '/')
|
||||||
|
|
||||||
|
let end = normalized.length
|
||||||
|
while (end > 0 && normalized[end - 1] === '/') {
|
||||||
|
end--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = end - 1
|
||||||
|
while (start >= 0 && normalized[start] !== '/') {
|
||||||
|
start--
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.slice(start + 1, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a character is a hex digit [0-9A-Fa-f]
|
||||||
|
*
|
||||||
|
* @param {string} char
|
||||||
|
* @return {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function isHexDigit (char) {
|
||||||
|
const code = char.charCodeAt(0)
|
||||||
|
return (
|
||||||
|
(code >= 48 && code <= 57) || // 0-9
|
||||||
|
(code >= 65 && code <= 70) || // A-F
|
||||||
|
(code >= 97 && code <= 102) // a-f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string contains percent encoding escapes.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @return {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function hasHexEscape (str) {
|
||||||
|
const maxIndex = str.length - 3
|
||||||
|
let lastIndex = -1
|
||||||
|
|
||||||
|
while ((lastIndex = str.indexOf('%', lastIndex + 1)) !== -1 && lastIndex <= maxIndex) {
|
||||||
|
if (isHexDigit(str[lastIndex + 1]) && isHexDigit(str[lastIndex + 2])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode hex escapes in a string (e.g., %20 -> space)
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function decodeHexEscapes (str) {
|
||||||
|
const firstEscape = str.indexOf('%')
|
||||||
|
if (firstEscape === -1) return str
|
||||||
|
|
||||||
|
let result = str.slice(0, firstEscape)
|
||||||
|
for (let idx = firstEscape; idx < str.length; idx++) {
|
||||||
|
if (
|
||||||
|
str[idx] === '%' &&
|
||||||
|
idx + 2 < str.length &&
|
||||||
|
isHexDigit(str[idx + 1]) &&
|
||||||
|
isHexDigit(str[idx + 2])
|
||||||
|
) {
|
||||||
|
result += String.fromCharCode(Number.parseInt(str[idx + 1] + str[idx + 2], 16))
|
||||||
|
idx += 2
|
||||||
|
} else {
|
||||||
|
result += str[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "content-disposition",
|
||||||
|
"description": "Create and parse Content-Disposition header",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"content-disposition",
|
||||||
|
"http",
|
||||||
|
"rfc6266",
|
||||||
|
"res"
|
||||||
|
],
|
||||||
|
"repository": "jshttp/content-disposition",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"c8": "^10.1.2",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-standard": "^14.1.1",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-markdown": "^3.0.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^6.6.0",
|
||||||
|
"eslint-plugin-standard": "^4.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "node --test --test-reporter spec",
|
||||||
|
"test-ci": "c8 --reporter=lcovonly --reporter=text npm test",
|
||||||
|
"test-cov": "c8 --reporter=html --reporter=text npm test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
1.0.5 / 2023-01-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* perf: skip value escaping when unnecessary
|
||||||
|
|
||||||
|
1.0.4 / 2017-09-11
|
||||||
|
==================
|
||||||
|
|
||||||
|
* perf: skip parameter parsing when no parameters
|
||||||
|
|
||||||
|
1.0.3 / 2017-09-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* perf: remove argument reassignment
|
||||||
|
|
||||||
|
1.0.2 / 2016-05-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* perf: enable strict mode
|
||||||
|
|
||||||
|
1.0.1 / 2015-02-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Improve missing `Content-Type` header error message
|
||||||
|
|
||||||
|
1.0.0 / 2015-02-01
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Initial implementation, derived from `media-typer@0.3.0`
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Douglas Christopher Wilson
|
||||||
|
|
||||||
|
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,94 @@
|
||||||
|
# content-type
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Node.js Version][node-image]][node-url]
|
||||||
|
[![Build Status][ci-image]][ci-url]
|
||||||
|
[![Coverage Status][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Create and parse HTTP Content-Type header according to RFC 7231
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install content-type
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
var contentType = require('content-type')
|
||||||
|
```
|
||||||
|
|
||||||
|
### contentType.parse(string)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var obj = contentType.parse('image/svg+xml; charset=utf-8')
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse a `Content-Type` header. This will return an object with the following
|
||||||
|
properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`):
|
||||||
|
|
||||||
|
- `type`: The media type (the type and subtype, always lower case).
|
||||||
|
Example: `'image/svg+xml'`
|
||||||
|
|
||||||
|
- `parameters`: An object of the parameters in the media type (name of parameter
|
||||||
|
always lower case). Example: `{charset: 'utf-8'}`
|
||||||
|
|
||||||
|
Throws a `TypeError` if the string is missing or invalid.
|
||||||
|
|
||||||
|
### contentType.parse(req)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var obj = contentType.parse(req)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse the `Content-Type` header from the given `req`. Short-cut for
|
||||||
|
`contentType.parse(req.headers['content-type'])`.
|
||||||
|
|
||||||
|
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
||||||
|
|
||||||
|
### contentType.parse(res)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var obj = contentType.parse(res)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse the `Content-Type` header set on the given `res`. Short-cut for
|
||||||
|
`contentType.parse(res.getHeader('content-type'))`.
|
||||||
|
|
||||||
|
Throws a `TypeError` if the `Content-Type` header is missing or invalid.
|
||||||
|
|
||||||
|
### contentType.format(obj)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var str = contentType.format({
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
parameters: { charset: 'utf-8' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Format an object into a `Content-Type` header. This will return a string of the
|
||||||
|
content type for the given object with the following properties (examples are
|
||||||
|
shown that produce the string `'image/svg+xml; charset=utf-8'`):
|
||||||
|
|
||||||
|
- `type`: The media type (will be lower-cased). Example: `'image/svg+xml'`
|
||||||
|
|
||||||
|
- `parameters`: An object of the parameters in the media type (name of the
|
||||||
|
parameter will be lower-cased). Example: `{charset: 'utf-8'}`
|
||||||
|
|
||||||
|
Throws a `TypeError` if the object contains an invalid type or parameter names.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci
|
||||||
|
[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master
|
||||||
|
[node-image]: https://badgen.net/npm/node/content-type
|
||||||
|
[node-url]: https://nodejs.org/en/download
|
||||||
|
[npm-downloads-image]: https://badgen.net/npm/dm/content-type
|
||||||
|
[npm-url]: https://npmjs.org/package/content-type
|
||||||
|
[npm-version-image]: https://badgen.net/npm/v/content-type
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
/*!
|
||||||
|
* content-type
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
|
||||||
|
*
|
||||||
|
* parameter = token "=" ( token / quoted-string )
|
||||||
|
* token = 1*tchar
|
||||||
|
* tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||||
|
* / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||||
|
* / DIGIT / ALPHA
|
||||||
|
* ; any VCHAR, except delimiters
|
||||||
|
* quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
||||||
|
* qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
||||||
|
* obs-text = %x80-FF
|
||||||
|
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||||
|
*/
|
||||||
|
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex
|
||||||
|
var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
|
||||||
|
var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match quoted-pair in RFC 7230 sec 3.2.6
|
||||||
|
*
|
||||||
|
* quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||||
|
* obs-text = %x80-FF
|
||||||
|
*/
|
||||||
|
var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
|
||||||
|
*/
|
||||||
|
var QUOTE_REGEXP = /([\\"])/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match type in RFC 7231 sec 3.1.1.1
|
||||||
|
*
|
||||||
|
* media-type = type "/" subtype
|
||||||
|
* type = token
|
||||||
|
* subtype = token
|
||||||
|
*/
|
||||||
|
var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.format = format
|
||||||
|
exports.parse = parse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format object to media type.
|
||||||
|
*
|
||||||
|
* @param {object} obj
|
||||||
|
* @return {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function format (obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
throw new TypeError('argument obj is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = obj.parameters
|
||||||
|
var type = obj.type
|
||||||
|
|
||||||
|
if (!type || !TYPE_REGEXP.test(type)) {
|
||||||
|
throw new TypeError('invalid type')
|
||||||
|
}
|
||||||
|
|
||||||
|
var string = type
|
||||||
|
|
||||||
|
// append parameters
|
||||||
|
if (parameters && typeof parameters === 'object') {
|
||||||
|
var param
|
||||||
|
var params = Object.keys(parameters).sort()
|
||||||
|
|
||||||
|
for (var i = 0; i < params.length; i++) {
|
||||||
|
param = params[i]
|
||||||
|
|
||||||
|
if (!TOKEN_REGEXP.test(param)) {
|
||||||
|
throw new TypeError('invalid parameter name')
|
||||||
|
}
|
||||||
|
|
||||||
|
string += '; ' + param + '=' + qstring(parameters[param])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse media type to object.
|
||||||
|
*
|
||||||
|
* @param {string|object} string
|
||||||
|
* @return {Object}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function parse (string) {
|
||||||
|
if (!string) {
|
||||||
|
throw new TypeError('argument string is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
// support req/res-like objects as argument
|
||||||
|
var header = typeof string === 'object'
|
||||||
|
? getcontenttype(string)
|
||||||
|
: string
|
||||||
|
|
||||||
|
if (typeof header !== 'string') {
|
||||||
|
throw new TypeError('argument string is required to be a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = header.indexOf(';')
|
||||||
|
var type = index !== -1
|
||||||
|
? header.slice(0, index).trim()
|
||||||
|
: header.trim()
|
||||||
|
|
||||||
|
if (!TYPE_REGEXP.test(type)) {
|
||||||
|
throw new TypeError('invalid media type')
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = new ContentType(type.toLowerCase())
|
||||||
|
|
||||||
|
// parse parameters
|
||||||
|
if (index !== -1) {
|
||||||
|
var key
|
||||||
|
var match
|
||||||
|
var value
|
||||||
|
|
||||||
|
PARAM_REGEXP.lastIndex = index
|
||||||
|
|
||||||
|
while ((match = PARAM_REGEXP.exec(header))) {
|
||||||
|
if (match.index !== index) {
|
||||||
|
throw new TypeError('invalid parameter format')
|
||||||
|
}
|
||||||
|
|
||||||
|
index += match[0].length
|
||||||
|
key = match[1].toLowerCase()
|
||||||
|
value = match[2]
|
||||||
|
|
||||||
|
if (value.charCodeAt(0) === 0x22 /* " */) {
|
||||||
|
// remove quotes
|
||||||
|
value = value.slice(1, -1)
|
||||||
|
|
||||||
|
// remove escapes
|
||||||
|
if (value.indexOf('\\') !== -1) {
|
||||||
|
value = value.replace(QESC_REGEXP, '$1')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.parameters[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index !== header.length) {
|
||||||
|
throw new TypeError('invalid parameter format')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content-type from req/res objects.
|
||||||
|
*
|
||||||
|
* @param {object}
|
||||||
|
* @return {Object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getcontenttype (obj) {
|
||||||
|
var header
|
||||||
|
|
||||||
|
if (typeof obj.getHeader === 'function') {
|
||||||
|
// res-like
|
||||||
|
header = obj.getHeader('content-type')
|
||||||
|
} else if (typeof obj.headers === 'object') {
|
||||||
|
// req-like
|
||||||
|
header = obj.headers && obj.headers['content-type']
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof header !== 'string') {
|
||||||
|
throw new TypeError('content-type header is missing from object')
|
||||||
|
}
|
||||||
|
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote a string if necessary.
|
||||||
|
*
|
||||||
|
* @param {string} val
|
||||||
|
* @return {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function qstring (val) {
|
||||||
|
var str = String(val)
|
||||||
|
|
||||||
|
// no need to quote tokens
|
||||||
|
if (TOKEN_REGEXP.test(str)) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.length > 0 && !TEXT_REGEXP.test(str)) {
|
||||||
|
throw new TypeError('invalid parameter value')
|
||||||
|
}
|
||||||
|
|
||||||
|
return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to represent a content type.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function ContentType (type) {
|
||||||
|
this.parameters = Object.create(null)
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "content-type",
|
||||||
|
"description": "Create and parse HTTP Content-Type header",
|
||||||
|
"version": "1.0.5",
|
||||||
|
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"content-type",
|
||||||
|
"http",
|
||||||
|
"req",
|
||||||
|
"res",
|
||||||
|
"rfc7231"
|
||||||
|
],
|
||||||
|
"repository": "jshttp/content-type",
|
||||||
|
"devDependencies": {
|
||||||
|
"deep-equal": "1.0.1",
|
||||||
|
"eslint": "8.32.0",
|
||||||
|
"eslint-config-standard": "15.0.1",
|
||||||
|
"eslint-plugin-import": "2.27.5",
|
||||||
|
"eslint-plugin-node": "11.1.0",
|
||||||
|
"eslint-plugin-promise": "6.1.1",
|
||||||
|
"eslint-plugin-standard": "4.1.0",
|
||||||
|
"mocha": "10.2.0",
|
||||||
|
"nyc": "15.1.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"LICENSE",
|
||||||
|
"HISTORY.md",
|
||||||
|
"README.md",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --reporter spec --check-leaks --bail test/",
|
||||||
|
"test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||||
|
"version": "node scripts/version-history.js && git add HISTORY.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
1.2.2 / 2024-10-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* various metadata/documentation tweaks (incl. #51)
|
||||||
|
|
||||||
|
|
||||||
|
1.2.1 / 2023-02-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* update annotations for allowed secret key types (#44, thanks @jyasskin!)
|
||||||
|
|
||||||
|
|
||||||
|
1.2.0 / 2022-02-17
|
||||||
|
==================
|
||||||
|
|
||||||
|
* allow buffer and other node-supported types as key (#33)
|
||||||
|
* be pickier about extra content after signed portion (#40)
|
||||||
|
* some internal code clarity/cleanup improvements (#26)
|
||||||
|
|
||||||
|
|
||||||
|
1.1.0 / 2018-01-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* switch to built-in `crypto.timingSafeEqual` for validation instead of previous double-hash method (thank you @jodevsa!)
|
||||||
|
|
||||||
|
|
||||||
|
1.0.7 / 2023-04-12
|
||||||
|
==================
|
||||||
|
|
||||||
|
Later release for older node.js versions. See the [v1.0.x branch notes](https://github.com/tj/node-cookie-signature/blob/v1.0.x/History.md#107--2023-04-12).
|
||||||
|
|
||||||
|
|
||||||
|
1.0.6 / 2015-02-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* use `npm test` instead of `make test` to run tests
|
||||||
|
* clearer assertion messages when checking input
|
||||||
|
|
||||||
|
|
||||||
|
1.0.5 / 2014-09-05
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add license to package.json
|
||||||
|
|
||||||
|
1.0.4 / 2014-06-25
|
||||||
|
==================
|
||||||
|
|
||||||
|
* corrected avoidance of timing attacks (thanks @tenbits!)
|
||||||
|
|
||||||
|
1.0.3 / 2014-01-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* [incorrect] fix for timing attacks
|
||||||
|
|
||||||
|
1.0.2 / 2014-01-28
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix missing repository warning
|
||||||
|
* fix typo in test
|
||||||
|
|
||||||
|
1.0.1 / 2013-04-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Revert "Changed underlying HMAC algo. to sha512."
|
||||||
|
* Revert "Fix for timing attacks on MAC verification."
|
||||||
|
|
||||||
|
0.0.1 / 2010-01-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2012–2024 LearnBoost <tj@learnboost.com> and other 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,23 @@
|
||||||
|
|
||||||
|
# cookie-signature
|
||||||
|
|
||||||
|
Sign and unsign cookies.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
var cookie = require('cookie-signature');
|
||||||
|
|
||||||
|
var val = cookie.sign('hello', 'tobiiscool');
|
||||||
|
val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI');
|
||||||
|
|
||||||
|
var val = cookie.sign('hello', 'tobiiscool');
|
||||||
|
cookie.unsign(val, 'tobiiscool').should.equal('hello');
|
||||||
|
cookie.unsign(val, 'luna').should.be.false;
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT.
|
||||||
|
|
||||||
|
See LICENSE file for details.
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign the given `val` with `secret`.
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.sign = function(val, secret){
|
||||||
|
if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string.");
|
||||||
|
if (null == secret) throw new TypeError("Secret key must be provided.");
|
||||||
|
return val + '.' + crypto
|
||||||
|
.createHmac('sha256', secret)
|
||||||
|
.update(val)
|
||||||
|
.digest('base64')
|
||||||
|
.replace(/\=+$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsign and decode the given `input` with `secret`,
|
||||||
|
* returning `false` if the signature is invalid.
|
||||||
|
*
|
||||||
|
* @param {String} input
|
||||||
|
* @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret
|
||||||
|
* @return {String|Boolean}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.unsign = function(input, secret){
|
||||||
|
if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided.");
|
||||||
|
if (null == secret) throw new TypeError("Secret key must be provided.");
|
||||||
|
var tentativeValue = input.slice(0, input.lastIndexOf('.')),
|
||||||
|
expectedInput = exports.sign(tentativeValue, secret),
|
||||||
|
expectedBuffer = Buffer.from(expectedInput),
|
||||||
|
inputBuffer = Buffer.from(input);
|
||||||
|
return (
|
||||||
|
expectedBuffer.length === inputBuffer.length &&
|
||||||
|
crypto.timingSafeEqual(expectedBuffer, inputBuffer)
|
||||||
|
) ? tentativeValue : false;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "cookie-signature",
|
||||||
|
"version": "1.2.2",
|
||||||
|
"main": "index.js",
|
||||||
|
"description": "Sign and unsign cookies",
|
||||||
|
"keywords": ["cookie", "sign", "unsign"],
|
||||||
|
"author": "TJ Holowaychuk <tj@learnboost.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/visionmedia/node-cookie-signature.git"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "*",
|
||||||
|
"should": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha --require should --reporter spec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2012-2014 Roman Shtylman <shtylman@gmail.com>
|
||||||
|
Copyright (c) 2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
||||||
|
|
||||||
|
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,317 @@
|
||||||
|
# cookie
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Node.js Version][node-image]][node-url]
|
||||||
|
[![Build Status][ci-image]][ci-url]
|
||||||
|
[![Coverage Status][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Basic HTTP cookie parser and serializer for HTTP servers.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This is a [Node.js](https://nodejs.org/en/) module available through the
|
||||||
|
[npm registry](https://www.npmjs.com/). Installation is done using the
|
||||||
|
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```js
|
||||||
|
var cookie = require('cookie');
|
||||||
|
```
|
||||||
|
|
||||||
|
### cookie.parse(str, options)
|
||||||
|
|
||||||
|
Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs.
|
||||||
|
The `str` argument is the string representing a `Cookie` header value and `options` is an
|
||||||
|
optional object containing additional parsing options.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2');
|
||||||
|
// { foo: 'bar', equation: 'E=mc^2' }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
`cookie.parse` accepts these properties in the options object.
|
||||||
|
|
||||||
|
##### decode
|
||||||
|
|
||||||
|
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
|
||||||
|
has a limited character set (and must be a simple string), this function can be used to decode
|
||||||
|
a previously-encoded cookie value into a JavaScript string or other object.
|
||||||
|
|
||||||
|
The default function is the global `decodeURIComponent`, which will decode any URL-encoded
|
||||||
|
sequences into their byte representations.
|
||||||
|
|
||||||
|
**note** if an error is thrown from this function, the original, non-decoded cookie value will
|
||||||
|
be returned as the cookie's value.
|
||||||
|
|
||||||
|
### cookie.serialize(name, value, options)
|
||||||
|
|
||||||
|
Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the
|
||||||
|
name for the cookie, the `value` argument is the value to set the cookie to, and the `options`
|
||||||
|
argument is an optional object containing additional serialization options.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var setCookie = cookie.serialize('foo', 'bar');
|
||||||
|
// foo=bar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
`cookie.serialize` accepts these properties in the options object.
|
||||||
|
|
||||||
|
##### domain
|
||||||
|
|
||||||
|
Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no
|
||||||
|
domain is set, and most clients will consider the cookie to apply to only the current domain.
|
||||||
|
|
||||||
|
##### encode
|
||||||
|
|
||||||
|
Specifies a function that will be used to encode a cookie's value. Since value of a cookie
|
||||||
|
has a limited character set (and must be a simple string), this function can be used to encode
|
||||||
|
a value into a string suited for a cookie's value.
|
||||||
|
|
||||||
|
The default function is the global `encodeURIComponent`, which will encode a JavaScript string
|
||||||
|
into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range.
|
||||||
|
|
||||||
|
##### expires
|
||||||
|
|
||||||
|
Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1].
|
||||||
|
By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and
|
||||||
|
will delete it on a condition like exiting a web browser application.
|
||||||
|
|
||||||
|
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
|
||||||
|
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||||
|
so if both are set, they should point to the same date and time.
|
||||||
|
|
||||||
|
##### httpOnly
|
||||||
|
|
||||||
|
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy,
|
||||||
|
the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||||
|
|
||||||
|
**note** be careful when setting this to `true`, as compliant clients will not allow client-side
|
||||||
|
JavaScript to see the cookie in `document.cookie`.
|
||||||
|
|
||||||
|
##### maxAge
|
||||||
|
|
||||||
|
Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2].
|
||||||
|
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
|
||||||
|
|
||||||
|
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
|
||||||
|
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||||
|
so if both are set, they should point to the same date and time.
|
||||||
|
|
||||||
|
##### partitioned
|
||||||
|
|
||||||
|
Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies)
|
||||||
|
attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the
|
||||||
|
`Partitioned` attribute is not set.
|
||||||
|
|
||||||
|
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||||
|
This also means many clients may ignore this attribute until they understand it.
|
||||||
|
|
||||||
|
More information about can be found in [the proposal](https://github.com/privacycg/CHIPS).
|
||||||
|
|
||||||
|
##### path
|
||||||
|
|
||||||
|
Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path
|
||||||
|
is considered the ["default path"][rfc-6265-5.1.4].
|
||||||
|
|
||||||
|
##### priority
|
||||||
|
|
||||||
|
Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
|
||||||
|
|
||||||
|
- `'low'` will set the `Priority` attribute to `Low`.
|
||||||
|
- `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
|
||||||
|
- `'high'` will set the `Priority` attribute to `High`.
|
||||||
|
|
||||||
|
More information about the different priority levels can be found in
|
||||||
|
[the specification][rfc-west-cookie-priority-00-4.1].
|
||||||
|
|
||||||
|
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||||
|
This also means many clients may ignore this attribute until they understand it.
|
||||||
|
|
||||||
|
##### sameSite
|
||||||
|
|
||||||
|
Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-09-5.4.7].
|
||||||
|
|
||||||
|
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||||
|
- `false` will not set the `SameSite` attribute.
|
||||||
|
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
|
||||||
|
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
|
||||||
|
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||||
|
|
||||||
|
More information about the different enforcement levels can be found in
|
||||||
|
[the specification][rfc-6265bis-09-5.4.7].
|
||||||
|
|
||||||
|
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||||
|
This also means many clients may ignore this attribute until they understand it.
|
||||||
|
|
||||||
|
##### secure
|
||||||
|
|
||||||
|
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy,
|
||||||
|
the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
|
||||||
|
|
||||||
|
**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to
|
||||||
|
the server in the future if the browser does not have an HTTPS connection.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following example uses this module in conjunction with the Node.js core HTTP server
|
||||||
|
to prompt a user for their name and display it back on future visits.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var cookie = require('cookie');
|
||||||
|
var escapeHtml = require('escape-html');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
function onRequest(req, res) {
|
||||||
|
// Parse the query string
|
||||||
|
var query = url.parse(req.url, true, true).query;
|
||||||
|
|
||||||
|
if (query && query.name) {
|
||||||
|
// Set a new cookie with the name
|
||||||
|
res.setHeader('Set-Cookie', cookie.serialize('name', String(query.name), {
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Redirect back after setting cookie
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.setHeader('Location', req.headers.referer || '/');
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the cookies on the request
|
||||||
|
var cookies = cookie.parse(req.headers.cookie || '');
|
||||||
|
|
||||||
|
// Get the visitor name set in the cookie
|
||||||
|
var name = cookies.name;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
res.write('<p>Welcome back, <b>' + escapeHtml(name) + '</b>!</p>');
|
||||||
|
} else {
|
||||||
|
res.write('<p>Hello, new visitor!</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.write('<form method="GET">');
|
||||||
|
res.write('<input placeholder="enter your name" name="name"> <input type="submit" value="Set Name">');
|
||||||
|
res.end('</form>');
|
||||||
|
}
|
||||||
|
|
||||||
|
http.createServer(onRequest).listen(3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benchmark
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run bench
|
||||||
|
|
||||||
|
> cookie@0.5.0 bench
|
||||||
|
> node benchmark/index.js
|
||||||
|
|
||||||
|
node@18.18.2
|
||||||
|
acorn@8.10.0
|
||||||
|
ada@2.6.0
|
||||||
|
ares@1.19.1
|
||||||
|
brotli@1.0.9
|
||||||
|
cldr@43.1
|
||||||
|
icu@73.2
|
||||||
|
llhttp@6.0.11
|
||||||
|
modules@108
|
||||||
|
napi@9
|
||||||
|
nghttp2@1.57.0
|
||||||
|
nghttp3@0.7.0
|
||||||
|
ngtcp2@0.8.1
|
||||||
|
openssl@3.0.10+quic
|
||||||
|
simdutf@3.2.14
|
||||||
|
tz@2023c
|
||||||
|
undici@5.26.3
|
||||||
|
unicode@15.0
|
||||||
|
uv@1.44.2
|
||||||
|
uvwasi@0.0.18
|
||||||
|
v8@10.2.154.26-node.26
|
||||||
|
zlib@1.2.13.1-motley
|
||||||
|
|
||||||
|
> node benchmark/parse-top.js
|
||||||
|
|
||||||
|
cookie.parse - top sites
|
||||||
|
|
||||||
|
14 tests completed.
|
||||||
|
|
||||||
|
parse accounts.google.com x 2,588,913 ops/sec ±0.74% (186 runs sampled)
|
||||||
|
parse apple.com x 2,370,002 ops/sec ±0.69% (186 runs sampled)
|
||||||
|
parse cloudflare.com x 2,213,102 ops/sec ±0.88% (188 runs sampled)
|
||||||
|
parse docs.google.com x 2,194,157 ops/sec ±1.03% (184 runs sampled)
|
||||||
|
parse drive.google.com x 2,265,084 ops/sec ±0.79% (187 runs sampled)
|
||||||
|
parse en.wikipedia.org x 457,099 ops/sec ±0.81% (186 runs sampled)
|
||||||
|
parse linkedin.com x 504,407 ops/sec ±0.89% (186 runs sampled)
|
||||||
|
parse maps.google.com x 1,230,959 ops/sec ±0.98% (186 runs sampled)
|
||||||
|
parse microsoft.com x 926,294 ops/sec ±0.88% (184 runs sampled)
|
||||||
|
parse play.google.com x 2,311,338 ops/sec ±0.83% (185 runs sampled)
|
||||||
|
parse support.google.com x 1,508,850 ops/sec ±0.86% (186 runs sampled)
|
||||||
|
parse www.google.com x 1,022,582 ops/sec ±1.32% (182 runs sampled)
|
||||||
|
parse youtu.be x 332,136 ops/sec ±1.02% (185 runs sampled)
|
||||||
|
parse youtube.com x 323,833 ops/sec ±0.77% (183 runs sampled)
|
||||||
|
|
||||||
|
> node benchmark/parse.js
|
||||||
|
|
||||||
|
cookie.parse - generic
|
||||||
|
|
||||||
|
6 tests completed.
|
||||||
|
|
||||||
|
simple x 3,214,032 ops/sec ±1.61% (183 runs sampled)
|
||||||
|
decode x 587,237 ops/sec ±1.16% (187 runs sampled)
|
||||||
|
unquote x 2,954,618 ops/sec ±1.35% (183 runs sampled)
|
||||||
|
duplicates x 857,008 ops/sec ±0.89% (187 runs sampled)
|
||||||
|
10 cookies x 292,133 ops/sec ±0.89% (187 runs sampled)
|
||||||
|
100 cookies x 22,610 ops/sec ±0.68% (187 runs sampled)
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [RFC 6265: HTTP State Management Mechanism][rfc-6265]
|
||||||
|
- [Same-site Cookies][rfc-6265bis-09-5.4.7]
|
||||||
|
|
||||||
|
[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/
|
||||||
|
[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1
|
||||||
|
[rfc-6265bis-09-5.4.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7
|
||||||
|
[rfc-6265]: https://tools.ietf.org/html/rfc6265
|
||||||
|
[rfc-6265-5.1.4]: https://tools.ietf.org/html/rfc6265#section-5.1.4
|
||||||
|
[rfc-6265-5.2.1]: https://tools.ietf.org/html/rfc6265#section-5.2.1
|
||||||
|
[rfc-6265-5.2.2]: https://tools.ietf.org/html/rfc6265#section-5.2.2
|
||||||
|
[rfc-6265-5.2.3]: https://tools.ietf.org/html/rfc6265#section-5.2.3
|
||||||
|
[rfc-6265-5.2.4]: https://tools.ietf.org/html/rfc6265#section-5.2.4
|
||||||
|
[rfc-6265-5.2.5]: https://tools.ietf.org/html/rfc6265#section-5.2.5
|
||||||
|
[rfc-6265-5.2.6]: https://tools.ietf.org/html/rfc6265#section-5.2.6
|
||||||
|
[rfc-6265-5.3]: https://tools.ietf.org/html/rfc6265#section-5.3
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[ci-image]: https://badgen.net/github/checks/jshttp/cookie/master?label=ci
|
||||||
|
[ci-url]: https://github.com/jshttp/cookie/actions/workflows/ci.yml
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/cookie/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/jshttp/cookie?branch=master
|
||||||
|
[node-image]: https://badgen.net/npm/node/cookie
|
||||||
|
[node-url]: https://nodejs.org/en/download
|
||||||
|
[npm-downloads-image]: https://badgen.net/npm/dm/cookie
|
||||||
|
[npm-url]: https://npmjs.org/package/cookie
|
||||||
|
[npm-version-image]: https://badgen.net/npm/v/cookie
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Security Policies and Procedures
|
||||||
|
|
||||||
|
## Reporting a Bug
|
||||||
|
|
||||||
|
The `cookie` team and community take all security bugs seriously. Thank
|
||||||
|
you for improving the security of the project. We appreciate your efforts and
|
||||||
|
responsible disclosure and will make every effort to acknowledge your
|
||||||
|
contributions.
|
||||||
|
|
||||||
|
Report security bugs by emailing the current owner(s) of `cookie`. This
|
||||||
|
information can be found in the npm registry using the command
|
||||||
|
`npm owner ls cookie`.
|
||||||
|
If unsure or unable to get the information from the above, open an issue
|
||||||
|
in the [project issue tracker](https://github.com/jshttp/cookie/issues)
|
||||||
|
asking for the current contact information.
|
||||||
|
|
||||||
|
To ensure the timely response to your report, please ensure that the entirety
|
||||||
|
of the report is contained within the email body and not solely behind a web
|
||||||
|
link or an attachment.
|
||||||
|
|
||||||
|
At least one owner will acknowledge your email within 48 hours, and will send a
|
||||||
|
more detailed response within 48 hours indicating the next steps in handling
|
||||||
|
your report. After the initial reply to your report, the owners will
|
||||||
|
endeavor to keep you informed of the progress towards a fix and full
|
||||||
|
announcement, and may ask for additional information or guidance.
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
/*!
|
||||||
|
* cookie
|
||||||
|
* Copyright(c) 2012-2014 Roman Shtylman
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.parse = parse;
|
||||||
|
exports.serialize = serialize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module variables.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var __toString = Object.prototype.toString
|
||||||
|
var __hasOwnProperty = Object.prototype.hasOwnProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
|
||||||
|
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
|
||||||
|
* which has been replaced by the token definition in RFC 7230 appendix B.
|
||||||
|
*
|
||||||
|
* cookie-name = token
|
||||||
|
* token = 1*tchar
|
||||||
|
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
|
||||||
|
* "*" / "+" / "-" / "." / "^" / "_" /
|
||||||
|
* "`" / "|" / "~" / DIGIT / ALPHA
|
||||||
|
*/
|
||||||
|
|
||||||
|
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
|
||||||
|
*
|
||||||
|
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
|
||||||
|
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||||
|
* ; US-ASCII characters excluding CTLs,
|
||||||
|
* ; whitespace DQUOTE, comma, semicolon,
|
||||||
|
* ; and backslash
|
||||||
|
*/
|
||||||
|
|
||||||
|
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match domain-value in RFC 6265 sec 4.1.1
|
||||||
|
*
|
||||||
|
* domain-value = <subdomain>
|
||||||
|
* ; defined in [RFC1034], Section 3.5, as
|
||||||
|
* ; enhanced by [RFC1123], Section 2.1
|
||||||
|
* <subdomain> = <label> | <subdomain> "." <label>
|
||||||
|
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
||||||
|
* Labels must be 63 characters or less.
|
||||||
|
* 'let-dig' not 'letter' in the first char, per RFC1123
|
||||||
|
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
||||||
|
* <let-dig-hyp> = <let-dig> | "-"
|
||||||
|
* <let-dig> = <letter> | <digit>
|
||||||
|
* <letter> = any one of the 52 alphabetic characters A through Z in
|
||||||
|
* upper case and a through z in lower case
|
||||||
|
* <digit> = any one of the ten digits 0 through 9
|
||||||
|
*
|
||||||
|
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
|
||||||
|
*
|
||||||
|
* > (Note that a leading %x2E ("."), if present, is ignored even though that
|
||||||
|
* character is not permitted, but a trailing %x2E ("."), if present, will
|
||||||
|
* cause the user agent to ignore the attribute.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegExp to match path-value in RFC 6265 sec 4.1.1
|
||||||
|
*
|
||||||
|
* path-value = <any CHAR except CTLs or ";">
|
||||||
|
* CHAR = %x01-7F
|
||||||
|
* ; defined in RFC 5234 appendix B.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a cookie header.
|
||||||
|
*
|
||||||
|
* Parse the given cookie header string into an object
|
||||||
|
* The object has the various cookies as keys(names) => values
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @param {object} [opt]
|
||||||
|
* @return {object}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function parse(str, opt) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
throw new TypeError('argument str must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = {};
|
||||||
|
var len = str.length;
|
||||||
|
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
|
||||||
|
if (len < 2) return obj;
|
||||||
|
|
||||||
|
var dec = (opt && opt.decode) || decode;
|
||||||
|
var index = 0;
|
||||||
|
var eqIdx = 0;
|
||||||
|
var endIdx = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
eqIdx = str.indexOf('=', index);
|
||||||
|
if (eqIdx === -1) break; // No more cookie pairs.
|
||||||
|
|
||||||
|
endIdx = str.indexOf(';', index);
|
||||||
|
|
||||||
|
if (endIdx === -1) {
|
||||||
|
endIdx = len;
|
||||||
|
} else if (eqIdx > endIdx) {
|
||||||
|
// backtrack on prior semicolon
|
||||||
|
index = str.lastIndexOf(';', eqIdx - 1) + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyStartIdx = startIndex(str, index, eqIdx);
|
||||||
|
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
|
||||||
|
var key = str.slice(keyStartIdx, keyEndIdx);
|
||||||
|
|
||||||
|
// only assign once
|
||||||
|
if (!__hasOwnProperty.call(obj, key)) {
|
||||||
|
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
|
||||||
|
var valEndIdx = endIndex(str, endIdx, valStartIdx);
|
||||||
|
|
||||||
|
if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
|
||||||
|
valStartIdx++;
|
||||||
|
valEndIdx--;
|
||||||
|
}
|
||||||
|
|
||||||
|
var val = str.slice(valStartIdx, valEndIdx);
|
||||||
|
obj[key] = tryDecode(val, dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
index = endIdx + 1
|
||||||
|
} while (index < len);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startIndex(str, index, max) {
|
||||||
|
do {
|
||||||
|
var code = str.charCodeAt(index);
|
||||||
|
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index;
|
||||||
|
} while (++index < max);
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endIndex(str, index, min) {
|
||||||
|
while (index > min) {
|
||||||
|
var code = str.charCodeAt(--index);
|
||||||
|
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1;
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize data into a cookie header.
|
||||||
|
*
|
||||||
|
* Serialize a name value pair into a cookie string suitable for
|
||||||
|
* http headers. An optional options object specifies cookie parameters.
|
||||||
|
*
|
||||||
|
* serialize('foo', 'bar', { httpOnly: true })
|
||||||
|
* => "foo=bar; httpOnly"
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} val
|
||||||
|
* @param {object} [opt]
|
||||||
|
* @return {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function serialize(name, val, opt) {
|
||||||
|
var enc = (opt && opt.encode) || encodeURIComponent;
|
||||||
|
|
||||||
|
if (typeof enc !== 'function') {
|
||||||
|
throw new TypeError('option encode is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieNameRegExp.test(name)) {
|
||||||
|
throw new TypeError('argument name is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = enc(val);
|
||||||
|
|
||||||
|
if (!cookieValueRegExp.test(value)) {
|
||||||
|
throw new TypeError('argument val is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
var str = name + '=' + value;
|
||||||
|
if (!opt) return str;
|
||||||
|
|
||||||
|
if (null != opt.maxAge) {
|
||||||
|
var maxAge = Math.floor(opt.maxAge);
|
||||||
|
|
||||||
|
if (!isFinite(maxAge)) {
|
||||||
|
throw new TypeError('option maxAge is invalid')
|
||||||
|
}
|
||||||
|
|
||||||
|
str += '; Max-Age=' + maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.domain) {
|
||||||
|
if (!domainValueRegExp.test(opt.domain)) {
|
||||||
|
throw new TypeError('option domain is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
str += '; Domain=' + opt.domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.path) {
|
||||||
|
if (!pathValueRegExp.test(opt.path)) {
|
||||||
|
throw new TypeError('option path is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
str += '; Path=' + opt.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.expires) {
|
||||||
|
var expires = opt.expires
|
||||||
|
|
||||||
|
if (!isDate(expires) || isNaN(expires.valueOf())) {
|
||||||
|
throw new TypeError('option expires is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
str += '; Expires=' + expires.toUTCString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.httpOnly) {
|
||||||
|
str += '; HttpOnly';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.secure) {
|
||||||
|
str += '; Secure';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.partitioned) {
|
||||||
|
str += '; Partitioned'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.priority) {
|
||||||
|
var priority = typeof opt.priority === 'string'
|
||||||
|
? opt.priority.toLowerCase() : opt.priority;
|
||||||
|
|
||||||
|
switch (priority) {
|
||||||
|
case 'low':
|
||||||
|
str += '; Priority=Low'
|
||||||
|
break
|
||||||
|
case 'medium':
|
||||||
|
str += '; Priority=Medium'
|
||||||
|
break
|
||||||
|
case 'high':
|
||||||
|
str += '; Priority=High'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new TypeError('option priority is invalid')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt.sameSite) {
|
||||||
|
var sameSite = typeof opt.sameSite === 'string'
|
||||||
|
? opt.sameSite.toLowerCase() : opt.sameSite;
|
||||||
|
|
||||||
|
switch (sameSite) {
|
||||||
|
case true:
|
||||||
|
str += '; SameSite=Strict';
|
||||||
|
break;
|
||||||
|
case 'lax':
|
||||||
|
str += '; SameSite=Lax';
|
||||||
|
break;
|
||||||
|
case 'strict':
|
||||||
|
str += '; SameSite=Strict';
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
str += '; SameSite=None';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new TypeError('option sameSite is invalid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL-decode string value. Optimized to skip native call when no %.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function decode (str) {
|
||||||
|
return str.indexOf('%') !== -1
|
||||||
|
? decodeURIComponent(str)
|
||||||
|
: str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if value is a Date.
|
||||||
|
*
|
||||||
|
* @param {*} val
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isDate (val) {
|
||||||
|
return __toString.call(val) === '[object Date]';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try decoding a string using a decoding function.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @param {function} decode
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function tryDecode(str, decode) {
|
||||||
|
try {
|
||||||
|
return decode(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "cookie",
|
||||||
|
"description": "HTTP server cookie parsing and serialization",
|
||||||
|
"version": "0.7.2",
|
||||||
|
"author": "Roman Shtylman <shtylman@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Douglas Christopher Wilson <doug@somethingdoug.com>"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"cookie",
|
||||||
|
"cookies"
|
||||||
|
],
|
||||||
|
"repository": "jshttp/cookie",
|
||||||
|
"devDependencies": {
|
||||||
|
"beautify-benchmark": "0.2.4",
|
||||||
|
"benchmark": "2.1.4",
|
||||||
|
"eslint": "8.53.0",
|
||||||
|
"eslint-plugin-markdown": "3.0.1",
|
||||||
|
"mocha": "10.2.0",
|
||||||
|
"nyc": "15.1.0",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"top-sites": "1.1.194"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"HISTORY.md",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"SECURITY.md",
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"bench": "node benchmark/index.js",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"test": "mocha --reporter spec --bail --check-leaks test/",
|
||||||
|
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
|
||||||
|
"test-cov": "nyc --reporter=html --reporter=text npm test",
|
||||||
|
"update-bench": "node scripts/update-benchmark.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
Copyright (c) 2018-2021 Josh Junon
|
||||||
|
|
||||||
|
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,481 @@
|
||||||
|
# debug
|
||||||
|
[](#backers)
|
||||||
|
[](#sponsors)
|
||||||
|
|
||||||
|
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
|
||||||
|
|
||||||
|
A tiny JavaScript debugging utility modelled after Node.js core's debugging
|
||||||
|
technique. Works in Node.js and web browsers.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`debug` exposes a function; simply pass this function the name of your module, and it will return a decorated version of `console.error` for you to pass debug statements to. This will allow you to toggle the debug output for different parts of your module as well as the module as a whole.
|
||||||
|
|
||||||
|
Example [_app.js_](./examples/node/app.js):
|
||||||
|
|
||||||
|
```js
|
||||||
|
var debug = require('debug')('http')
|
||||||
|
, http = require('http')
|
||||||
|
, name = 'My App';
|
||||||
|
|
||||||
|
// fake app
|
||||||
|
|
||||||
|
debug('booting %o', name);
|
||||||
|
|
||||||
|
http.createServer(function(req, res){
|
||||||
|
debug(req.method + ' ' + req.url);
|
||||||
|
res.end('hello\n');
|
||||||
|
}).listen(3000, function(){
|
||||||
|
debug('listening');
|
||||||
|
});
|
||||||
|
|
||||||
|
// fake worker of some kind
|
||||||
|
|
||||||
|
require('./worker');
|
||||||
|
```
|
||||||
|
|
||||||
|
Example [_worker.js_](./examples/node/worker.js):
|
||||||
|
|
||||||
|
```js
|
||||||
|
var a = require('debug')('worker:a')
|
||||||
|
, b = require('debug')('worker:b');
|
||||||
|
|
||||||
|
function work() {
|
||||||
|
a('doing lots of uninteresting work');
|
||||||
|
setTimeout(work, Math.random() * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
work();
|
||||||
|
|
||||||
|
function workb() {
|
||||||
|
b('doing some work');
|
||||||
|
setTimeout(workb, Math.random() * 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
workb();
|
||||||
|
```
|
||||||
|
|
||||||
|
The `DEBUG` environment variable is then used to enable these based on space or
|
||||||
|
comma-delimited names.
|
||||||
|
|
||||||
|
Here are some examples:
|
||||||
|
|
||||||
|
<img width="647" alt="screen shot 2017-08-08 at 12 53 04 pm" src="https://user-images.githubusercontent.com/71256/29091703-a6302cdc-7c38-11e7-8304-7c0b3bc600cd.png">
|
||||||
|
<img width="647" alt="screen shot 2017-08-08 at 12 53 38 pm" src="https://user-images.githubusercontent.com/71256/29091700-a62a6888-7c38-11e7-800b-db911291ca2b.png">
|
||||||
|
<img width="647" alt="screen shot 2017-08-08 at 12 53 25 pm" src="https://user-images.githubusercontent.com/71256/29091701-a62ea114-7c38-11e7-826a-2692bedca740.png">
|
||||||
|
|
||||||
|
#### Windows command prompt notes
|
||||||
|
|
||||||
|
##### CMD
|
||||||
|
|
||||||
|
On Windows the environment variable is set using the `set` command.
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
set DEBUG=*,-not_this
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
set DEBUG=* & node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
##### PowerShell (VS Code default)
|
||||||
|
|
||||||
|
PowerShell uses different syntax to set environment variables.
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
$env:DEBUG = "*,-not_this"
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
$env:DEBUG='app';node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run the program to be debugged as usual.
|
||||||
|
|
||||||
|
npm script example:
|
||||||
|
```js
|
||||||
|
"windowsDebug": "@powershell -Command $env:DEBUG='*';node app.js",
|
||||||
|
```
|
||||||
|
|
||||||
|
## Namespace Colors
|
||||||
|
|
||||||
|
Every debug instance has a color generated for it based on its namespace name.
|
||||||
|
This helps when visually parsing the debug output to identify which debug instance
|
||||||
|
a debug line belongs to.
|
||||||
|
|
||||||
|
#### Node.js
|
||||||
|
|
||||||
|
In Node.js, colors are enabled when stderr is a TTY. You also _should_ install
|
||||||
|
the [`supports-color`](https://npmjs.org/supports-color) module alongside debug,
|
||||||
|
otherwise debug will only use a small handful of basic colors.
|
||||||
|
|
||||||
|
<img width="521" src="https://user-images.githubusercontent.com/71256/29092181-47f6a9e6-7c3a-11e7-9a14-1928d8a711cd.png">
|
||||||
|
|
||||||
|
#### Web Browser
|
||||||
|
|
||||||
|
Colors are also enabled on "Web Inspectors" that understand the `%c` formatting
|
||||||
|
option. These are WebKit web inspectors, Firefox ([since version
|
||||||
|
31](https://hacks.mozilla.org/2014/05/editable-box-model-multiple-selection-sublime-text-keys-much-more-firefox-developer-tools-episode-31/))
|
||||||
|
and the Firebug plugin for Firefox (any version).
|
||||||
|
|
||||||
|
<img width="524" src="https://user-images.githubusercontent.com/71256/29092033-b65f9f2e-7c39-11e7-8e32-f6f0d8e865c1.png">
|
||||||
|
|
||||||
|
|
||||||
|
## Millisecond diff
|
||||||
|
|
||||||
|
When actively developing an application it can be useful to see when the time spent between one `debug()` call and the next. Suppose for example you invoke `debug()` before requesting a resource, and after as well, the "+NNNms" will show you how much time was spent between calls.
|
||||||
|
|
||||||
|
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
|
||||||
|
|
||||||
|
When stdout is not a TTY, `Date#toISOString()` is used, making it more useful for logging the debug information as shown below:
|
||||||
|
|
||||||
|
<img width="647" src="https://user-images.githubusercontent.com/71256/29091956-6bd78372-7c39-11e7-8c55-c948396d6edd.png">
|
||||||
|
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
If you're using this in one or more of your libraries, you _should_ use the name of your library so that developers may toggle debugging as desired without guessing names. If you have more than one debuggers you _should_ prefix them with your library name and use ":" to separate features. For example "bodyParser" from Connect would then be "connect:bodyParser". If you append a "*" to the end of your name, it will always be enabled regardless of the setting of the DEBUG environment variable. You can then use it for normal output as well as debug output.
|
||||||
|
|
||||||
|
## Wildcards
|
||||||
|
|
||||||
|
The `*` character may be used as a wildcard. Suppose for example your library has
|
||||||
|
debuggers named "connect:bodyParser", "connect:compress", "connect:session",
|
||||||
|
instead of listing all three with
|
||||||
|
`DEBUG=connect:bodyParser,connect:compress,connect:session`, you may simply do
|
||||||
|
`DEBUG=connect:*`, or to run everything using this module simply use `DEBUG=*`.
|
||||||
|
|
||||||
|
You can also exclude specific debuggers by prefixing them with a "-" character.
|
||||||
|
For example, `DEBUG=*,-connect:*` would include all debuggers except those
|
||||||
|
starting with "connect:".
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
When running through Node.js, you can set a few environment variables that will
|
||||||
|
change the behavior of the debug logging:
|
||||||
|
|
||||||
|
| Name | Purpose |
|
||||||
|
|-----------|-------------------------------------------------|
|
||||||
|
| `DEBUG` | Enables/disables specific debugging namespaces. |
|
||||||
|
| `DEBUG_HIDE_DATE` | Hide date from debug output (non-TTY). |
|
||||||
|
| `DEBUG_COLORS`| Whether or not to use colors in the debug output. |
|
||||||
|
| `DEBUG_DEPTH` | Object inspection depth. |
|
||||||
|
| `DEBUG_SHOW_HIDDEN` | Shows hidden properties on inspected objects. |
|
||||||
|
|
||||||
|
|
||||||
|
__Note:__ The environment variables beginning with `DEBUG_` end up being
|
||||||
|
converted into an Options object that gets used with `%o`/`%O` formatters.
|
||||||
|
See the Node.js documentation for
|
||||||
|
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options)
|
||||||
|
for the complete list.
|
||||||
|
|
||||||
|
## Formatters
|
||||||
|
|
||||||
|
Debug uses [printf-style](https://wikipedia.org/wiki/Printf_format_string) formatting.
|
||||||
|
Below are the officially supported formatters:
|
||||||
|
|
||||||
|
| Formatter | Representation |
|
||||||
|
|-----------|----------------|
|
||||||
|
| `%O` | Pretty-print an Object on multiple lines. |
|
||||||
|
| `%o` | Pretty-print an Object all on a single line. |
|
||||||
|
| `%s` | String. |
|
||||||
|
| `%d` | Number (both integer and float). |
|
||||||
|
| `%j` | JSON. Replaced with the string '[Circular]' if the argument contains circular references. |
|
||||||
|
| `%%` | Single percent sign ('%'). This does not consume an argument. |
|
||||||
|
|
||||||
|
|
||||||
|
### Custom formatters
|
||||||
|
|
||||||
|
You can add custom formatters by extending the `debug.formatters` object.
|
||||||
|
For example, if you wanted to add support for rendering a Buffer as hex with
|
||||||
|
`%h`, you could do something like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const createDebug = require('debug')
|
||||||
|
createDebug.formatters.h = (v) => {
|
||||||
|
return v.toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
// …elsewhere
|
||||||
|
const debug = createDebug('foo')
|
||||||
|
debug('this is hex: %h', new Buffer('hello world'))
|
||||||
|
// foo this is hex: 68656c6c6f20776f726c6421 +0ms
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
You can build a browser-ready script using [browserify](https://github.com/substack/node-browserify),
|
||||||
|
or just use the [browserify-as-a-service](https://wzrd.in/) [build](https://wzrd.in/standalone/debug@latest),
|
||||||
|
if you don't want to build it yourself.
|
||||||
|
|
||||||
|
Debug's enable state is currently persisted by `localStorage`.
|
||||||
|
Consider the situation shown below where you have `worker:a` and `worker:b`,
|
||||||
|
and wish to debug both. You can enable this using `localStorage.debug`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
localStorage.debug = 'worker:*'
|
||||||
|
```
|
||||||
|
|
||||||
|
And then refresh the page.
|
||||||
|
|
||||||
|
```js
|
||||||
|
a = debug('worker:a');
|
||||||
|
b = debug('worker:b');
|
||||||
|
|
||||||
|
setInterval(function(){
|
||||||
|
a('doing some work');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setInterval(function(){
|
||||||
|
b('doing some work');
|
||||||
|
}, 1200);
|
||||||
|
```
|
||||||
|
|
||||||
|
In Chromium-based web browsers (e.g. Brave, Chrome, and Electron), the JavaScript console will—by default—only show messages logged by `debug` if the "Verbose" log level is _enabled_.
|
||||||
|
|
||||||
|
<img width="647" src="https://user-images.githubusercontent.com/7143133/152083257-29034707-c42c-4959-8add-3cee850e6fcf.png">
|
||||||
|
|
||||||
|
## Output streams
|
||||||
|
|
||||||
|
By default `debug` will log to stderr, however this can be configured per-namespace by overriding the `log` method:
|
||||||
|
|
||||||
|
Example [_stdout.js_](./examples/node/stdout.js):
|
||||||
|
|
||||||
|
```js
|
||||||
|
var debug = require('debug');
|
||||||
|
var error = debug('app:error');
|
||||||
|
|
||||||
|
// by default stderr is used
|
||||||
|
error('goes to stderr!');
|
||||||
|
|
||||||
|
var log = debug('app:log');
|
||||||
|
// set this namespace to log via console.log
|
||||||
|
log.log = console.log.bind(console); // don't forget to bind to console!
|
||||||
|
log('goes to stdout');
|
||||||
|
error('still goes to stderr!');
|
||||||
|
|
||||||
|
// set all output to go via console.info
|
||||||
|
// overrides all per-namespace log settings
|
||||||
|
debug.log = console.info.bind(console);
|
||||||
|
error('now goes to stdout via console.info');
|
||||||
|
log('still goes to stdout, but via console.info now');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extend
|
||||||
|
You can simply extend debugger
|
||||||
|
```js
|
||||||
|
const log = require('debug')('auth');
|
||||||
|
|
||||||
|
//creates new debug instance with extended namespace
|
||||||
|
const logSign = log.extend('sign');
|
||||||
|
const logLogin = log.extend('login');
|
||||||
|
|
||||||
|
log('hello'); // auth hello
|
||||||
|
logSign('hello'); //auth:sign hello
|
||||||
|
logLogin('hello'); //auth:login hello
|
||||||
|
```
|
||||||
|
|
||||||
|
## Set dynamically
|
||||||
|
|
||||||
|
You can also enable debug dynamically by calling the `enable()` method :
|
||||||
|
|
||||||
|
```js
|
||||||
|
let debug = require('debug');
|
||||||
|
|
||||||
|
console.log(1, debug.enabled('test'));
|
||||||
|
|
||||||
|
debug.enable('test');
|
||||||
|
console.log(2, debug.enabled('test'));
|
||||||
|
|
||||||
|
debug.disable();
|
||||||
|
console.log(3, debug.enabled('test'));
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
print :
|
||||||
|
```
|
||||||
|
1 false
|
||||||
|
2 true
|
||||||
|
3 false
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
`enable(namespaces)`
|
||||||
|
`namespaces` can include modes separated by a colon and wildcards.
|
||||||
|
|
||||||
|
Note that calling `enable()` completely overrides previously set DEBUG variable :
|
||||||
|
|
||||||
|
```
|
||||||
|
$ DEBUG=foo node -e 'var dbg = require("debug"); dbg.enable("bar"); console.log(dbg.enabled("foo"))'
|
||||||
|
=> false
|
||||||
|
```
|
||||||
|
|
||||||
|
`disable()`
|
||||||
|
|
||||||
|
Will disable all namespaces. The functions returns the namespaces currently
|
||||||
|
enabled (and skipped). This can be useful if you want to disable debugging
|
||||||
|
temporarily without knowing what was enabled to begin with.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let debug = require('debug');
|
||||||
|
debug.enable('foo:*,-foo:bar');
|
||||||
|
let namespaces = debug.disable();
|
||||||
|
debug.enable(namespaces);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: There is no guarantee that the string will be identical to the initial
|
||||||
|
enable string, but semantically they will be identical.
|
||||||
|
|
||||||
|
## Checking whether a debug target is enabled
|
||||||
|
|
||||||
|
After you've created a debug instance, you can determine whether or not it is
|
||||||
|
enabled by checking the `enabled` property:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const debug = require('debug')('http');
|
||||||
|
|
||||||
|
if (debug.enabled) {
|
||||||
|
// do stuff...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also manually toggle this property to force the debug instance to be
|
||||||
|
enabled or disabled.
|
||||||
|
|
||||||
|
## Usage in child processes
|
||||||
|
|
||||||
|
Due to the way `debug` detects if the output is a TTY or not, colors are not shown in child processes when `stderr` is piped. A solution is to pass the `DEBUG_COLORS=1` environment variable to the child process.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
worker = fork(WORKER_WRAP_PATH, [workerPath], {
|
||||||
|
stdio: [
|
||||||
|
/* stdin: */ 0,
|
||||||
|
/* stdout: */ 'pipe',
|
||||||
|
/* stderr: */ 'pipe',
|
||||||
|
'ipc',
|
||||||
|
],
|
||||||
|
env: Object.assign({}, process.env, {
|
||||||
|
DEBUG_COLORS: 1 // without this settings, colors won't be shown
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.stderr.pipe(process.stderr, { end: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
- TJ Holowaychuk
|
||||||
|
- Nathan Rajlich
|
||||||
|
- Andrew Rhyne
|
||||||
|
- Josh Junon
|
||||||
|
|
||||||
|
## Backers
|
||||||
|
|
||||||
|
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/debug#backer)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/debug/backer/0/website" target="_blank"><img src="https://opencollective.com/debug/backer/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/1/website" target="_blank"><img src="https://opencollective.com/debug/backer/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/2/website" target="_blank"><img src="https://opencollective.com/debug/backer/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/3/website" target="_blank"><img src="https://opencollective.com/debug/backer/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/4/website" target="_blank"><img src="https://opencollective.com/debug/backer/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/5/website" target="_blank"><img src="https://opencollective.com/debug/backer/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/6/website" target="_blank"><img src="https://opencollective.com/debug/backer/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/7/website" target="_blank"><img src="https://opencollective.com/debug/backer/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/8/website" target="_blank"><img src="https://opencollective.com/debug/backer/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/9/website" target="_blank"><img src="https://opencollective.com/debug/backer/9/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/10/website" target="_blank"><img src="https://opencollective.com/debug/backer/10/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/11/website" target="_blank"><img src="https://opencollective.com/debug/backer/11/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/12/website" target="_blank"><img src="https://opencollective.com/debug/backer/12/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/13/website" target="_blank"><img src="https://opencollective.com/debug/backer/13/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/14/website" target="_blank"><img src="https://opencollective.com/debug/backer/14/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/15/website" target="_blank"><img src="https://opencollective.com/debug/backer/15/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/16/website" target="_blank"><img src="https://opencollective.com/debug/backer/16/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/17/website" target="_blank"><img src="https://opencollective.com/debug/backer/17/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/18/website" target="_blank"><img src="https://opencollective.com/debug/backer/18/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/19/website" target="_blank"><img src="https://opencollective.com/debug/backer/19/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/20/website" target="_blank"><img src="https://opencollective.com/debug/backer/20/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/21/website" target="_blank"><img src="https://opencollective.com/debug/backer/21/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/22/website" target="_blank"><img src="https://opencollective.com/debug/backer/22/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/23/website" target="_blank"><img src="https://opencollective.com/debug/backer/23/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/24/website" target="_blank"><img src="https://opencollective.com/debug/backer/24/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/25/website" target="_blank"><img src="https://opencollective.com/debug/backer/25/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/26/website" target="_blank"><img src="https://opencollective.com/debug/backer/26/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/27/website" target="_blank"><img src="https://opencollective.com/debug/backer/27/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/28/website" target="_blank"><img src="https://opencollective.com/debug/backer/28/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/backer/29/website" target="_blank"><img src="https://opencollective.com/debug/backer/29/avatar.svg"></a>
|
||||||
|
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/debug#sponsor)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/0/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/1/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/2/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/3/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/4/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/5/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/6/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/7/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/8/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/9/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/9/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/10/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/10/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/11/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/11/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/12/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/12/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/13/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/13/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/14/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/14/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/15/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/15/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/16/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/16/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/17/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/17/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/18/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/18/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/19/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/19/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/20/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/20/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/21/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/21/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/22/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/22/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/23/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/23/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/24/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/24/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/25/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/25/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/26/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/26/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/27/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/27/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/28/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/28/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/debug/sponsor/29/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/29/avatar.svg"></a>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
Copyright (c) 2018-2021 Josh Junon
|
||||||
|
|
||||||
|
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,64 @@
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"version": "4.4.3",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/debug-js/debug.git"
|
||||||
|
},
|
||||||
|
"description": "Lightweight debugging utility for Node.js and the browser",
|
||||||
|
"keywords": [
|
||||||
|
"debug",
|
||||||
|
"log",
|
||||||
|
"debugger"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"author": "Josh Junon (https://github.com/qix-)",
|
||||||
|
"contributors": [
|
||||||
|
"TJ Holowaychuk <tj@vision-media.ca>",
|
||||||
|
"Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)",
|
||||||
|
"Andrew Rhyne <rhyneandrew@gmail.com>"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "xo",
|
||||||
|
"test": "npm run test:node && npm run test:browser && npm run lint",
|
||||||
|
"test:node": "mocha test.js test.node.js",
|
||||||
|
"test:browser": "karma start --single-run",
|
||||||
|
"test:coverage": "cat ./coverage/lcov.info | coveralls"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"brfs": "^2.0.1",
|
||||||
|
"browserify": "^16.2.3",
|
||||||
|
"coveralls": "^3.0.2",
|
||||||
|
"karma": "^3.1.4",
|
||||||
|
"karma-browserify": "^6.0.0",
|
||||||
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-mocha": "^1.3.0",
|
||||||
|
"mocha": "^5.2.0",
|
||||||
|
"mocha-lcov-reporter": "^1.2.0",
|
||||||
|
"sinon": "^14.0.0",
|
||||||
|
"xo": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"browser": "./src/browser.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"xo": {
|
||||||
|
"rules": {
|
||||||
|
"import/extensions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the web browser implementation of `debug()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.formatArgs = formatArgs;
|
||||||
|
exports.save = save;
|
||||||
|
exports.load = load;
|
||||||
|
exports.useColors = useColors;
|
||||||
|
exports.storage = localstorage();
|
||||||
|
exports.destroy = (() => {
|
||||||
|
let warned = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!warned) {
|
||||||
|
warned = true;
|
||||||
|
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.colors = [
|
||||||
|
'#0000CC',
|
||||||
|
'#0000FF',
|
||||||
|
'#0033CC',
|
||||||
|
'#0033FF',
|
||||||
|
'#0066CC',
|
||||||
|
'#0066FF',
|
||||||
|
'#0099CC',
|
||||||
|
'#0099FF',
|
||||||
|
'#00CC00',
|
||||||
|
'#00CC33',
|
||||||
|
'#00CC66',
|
||||||
|
'#00CC99',
|
||||||
|
'#00CCCC',
|
||||||
|
'#00CCFF',
|
||||||
|
'#3300CC',
|
||||||
|
'#3300FF',
|
||||||
|
'#3333CC',
|
||||||
|
'#3333FF',
|
||||||
|
'#3366CC',
|
||||||
|
'#3366FF',
|
||||||
|
'#3399CC',
|
||||||
|
'#3399FF',
|
||||||
|
'#33CC00',
|
||||||
|
'#33CC33',
|
||||||
|
'#33CC66',
|
||||||
|
'#33CC99',
|
||||||
|
'#33CCCC',
|
||||||
|
'#33CCFF',
|
||||||
|
'#6600CC',
|
||||||
|
'#6600FF',
|
||||||
|
'#6633CC',
|
||||||
|
'#6633FF',
|
||||||
|
'#66CC00',
|
||||||
|
'#66CC33',
|
||||||
|
'#9900CC',
|
||||||
|
'#9900FF',
|
||||||
|
'#9933CC',
|
||||||
|
'#9933FF',
|
||||||
|
'#99CC00',
|
||||||
|
'#99CC33',
|
||||||
|
'#CC0000',
|
||||||
|
'#CC0033',
|
||||||
|
'#CC0066',
|
||||||
|
'#CC0099',
|
||||||
|
'#CC00CC',
|
||||||
|
'#CC00FF',
|
||||||
|
'#CC3300',
|
||||||
|
'#CC3333',
|
||||||
|
'#CC3366',
|
||||||
|
'#CC3399',
|
||||||
|
'#CC33CC',
|
||||||
|
'#CC33FF',
|
||||||
|
'#CC6600',
|
||||||
|
'#CC6633',
|
||||||
|
'#CC9900',
|
||||||
|
'#CC9933',
|
||||||
|
'#CCCC00',
|
||||||
|
'#CCCC33',
|
||||||
|
'#FF0000',
|
||||||
|
'#FF0033',
|
||||||
|
'#FF0066',
|
||||||
|
'#FF0099',
|
||||||
|
'#FF00CC',
|
||||||
|
'#FF00FF',
|
||||||
|
'#FF3300',
|
||||||
|
'#FF3333',
|
||||||
|
'#FF3366',
|
||||||
|
'#FF3399',
|
||||||
|
'#FF33CC',
|
||||||
|
'#FF33FF',
|
||||||
|
'#FF6600',
|
||||||
|
'#FF6633',
|
||||||
|
'#FF9900',
|
||||||
|
'#FF9933',
|
||||||
|
'#FFCC00',
|
||||||
|
'#FFCC33'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
|
||||||
|
* and the Firebug extension (any Firefox version) are known
|
||||||
|
* to support "%c" CSS customizations.
|
||||||
|
*
|
||||||
|
* TODO: add a `localStorage` variable to explicitly enable/disable colors
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
|
function useColors() {
|
||||||
|
// NB: In an Electron preload script, document will be defined but not fully
|
||||||
|
// initialized. Since we know we're in Chrome, we'll just detect this case
|
||||||
|
// explicitly
|
||||||
|
if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internet Explorer and Edge do not support colors.
|
||||||
|
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let m;
|
||||||
|
|
||||||
|
// Is webkit? http://stackoverflow.com/a/16459606/376773
|
||||||
|
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
|
||||||
|
// eslint-disable-next-line no-return-assign
|
||||||
|
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
|
||||||
|
// Is firebug? http://stackoverflow.com/a/398120/376773
|
||||||
|
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
|
||||||
|
// Is firefox >= v31?
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
|
||||||
|
(typeof navigator !== 'undefined' && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31) ||
|
||||||
|
// Double check webkit in userAgent just in case we are in a worker
|
||||||
|
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colorize log arguments if enabled.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatArgs(args) {
|
||||||
|
args[0] = (this.useColors ? '%c' : '') +
|
||||||
|
this.namespace +
|
||||||
|
(this.useColors ? ' %c' : ' ') +
|
||||||
|
args[0] +
|
||||||
|
(this.useColors ? '%c ' : ' ') +
|
||||||
|
'+' + module.exports.humanize(this.diff);
|
||||||
|
|
||||||
|
if (!this.useColors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = 'color: ' + this.color;
|
||||||
|
args.splice(1, 0, c, 'color: inherit');
|
||||||
|
|
||||||
|
// The final "%c" is somewhat tricky, because there could be other
|
||||||
|
// arguments passed either before or after the %c, so we need to
|
||||||
|
// figure out the correct index to insert the CSS into
|
||||||
|
let index = 0;
|
||||||
|
let lastC = 0;
|
||||||
|
args[0].replace(/%[a-zA-Z%]/g, match => {
|
||||||
|
if (match === '%%') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
if (match === '%c') {
|
||||||
|
// We only are interested in the *last* %c
|
||||||
|
// (the user may have provided their own)
|
||||||
|
lastC = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
args.splice(lastC, 0, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes `console.debug()` when available.
|
||||||
|
* No-op when `console.debug` is not a "function".
|
||||||
|
* If `console.debug` is not available, falls back
|
||||||
|
* to `console.log`.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
exports.log = console.debug || console.log || (() => {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save `namespaces`.
|
||||||
|
*
|
||||||
|
* @param {String} namespaces
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
function save(namespaces) {
|
||||||
|
try {
|
||||||
|
if (namespaces) {
|
||||||
|
exports.storage.setItem('debug', namespaces);
|
||||||
|
} else {
|
||||||
|
exports.storage.removeItem('debug');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Swallow
|
||||||
|
// XXX (@Qix-) should we be logging these?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load `namespaces`.
|
||||||
|
*
|
||||||
|
* @return {String} returns the previously persisted debug modes
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
function load() {
|
||||||
|
let r;
|
||||||
|
try {
|
||||||
|
r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
|
||||||
|
} catch (error) {
|
||||||
|
// Swallow
|
||||||
|
// XXX (@Qix-) should we be logging these?
|
||||||
|
}
|
||||||
|
|
||||||
|
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
|
||||||
|
if (!r && typeof process !== 'undefined' && 'env' in process) {
|
||||||
|
r = process.env.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localstorage attempts to return the localstorage.
|
||||||
|
*
|
||||||
|
* This is necessary because safari throws
|
||||||
|
* when a user disables cookies/localstorage
|
||||||
|
* and you attempt to access it.
|
||||||
|
*
|
||||||
|
* @return {LocalStorage}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function localstorage() {
|
||||||
|
try {
|
||||||
|
// TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context
|
||||||
|
// The Browser also has localStorage in the global context.
|
||||||
|
return localStorage;
|
||||||
|
} catch (error) {
|
||||||
|
// Swallow
|
||||||
|
// XXX (@Qix-) should we be logging these?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = require('./common')(exports);
|
||||||
|
|
||||||
|
const {formatters} = module.exports;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
formatters.j = function (v) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(v);
|
||||||
|
} catch (error) {
|
||||||
|
return '[UnexpectedJSONParseError]: ' + error.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the common logic for both the Node.js and web browser
|
||||||
|
* implementations of `debug()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function setup(env) {
|
||||||
|
createDebug.debug = createDebug;
|
||||||
|
createDebug.default = createDebug;
|
||||||
|
createDebug.coerce = coerce;
|
||||||
|
createDebug.disable = disable;
|
||||||
|
createDebug.enable = enable;
|
||||||
|
createDebug.enabled = enabled;
|
||||||
|
createDebug.humanize = require('ms');
|
||||||
|
createDebug.destroy = destroy;
|
||||||
|
|
||||||
|
Object.keys(env).forEach(key => {
|
||||||
|
createDebug[key] = env[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently active debug mode names, and names to skip.
|
||||||
|
*/
|
||||||
|
|
||||||
|
createDebug.names = [];
|
||||||
|
createDebug.skips = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of special "%n" handling functions, for the debug "format" argument.
|
||||||
|
*
|
||||||
|
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
|
||||||
|
*/
|
||||||
|
createDebug.formatters = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a color for a debug namespace
|
||||||
|
* @param {String} namespace The namespace string for the debug instance to be colored
|
||||||
|
* @return {Number|String} An ANSI color code for the given namespace
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
function selectColor(namespace) {
|
||||||
|
let hash = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < namespace.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash) + namespace.charCodeAt(i);
|
||||||
|
hash |= 0; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
|
||||||
|
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
|
||||||
|
}
|
||||||
|
createDebug.selectColor = selectColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a debugger with the given `namespace`.
|
||||||
|
*
|
||||||
|
* @param {String} namespace
|
||||||
|
* @return {Function}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
function createDebug(namespace) {
|
||||||
|
let prevTime;
|
||||||
|
let enableOverride = null;
|
||||||
|
let namespacesCache;
|
||||||
|
let enabledCache;
|
||||||
|
|
||||||
|
function debug(...args) {
|
||||||
|
// Disabled?
|
||||||
|
if (!debug.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const self = debug;
|
||||||
|
|
||||||
|
// Set `diff` timestamp
|
||||||
|
const curr = Number(new Date());
|
||||||
|
const ms = curr - (prevTime || curr);
|
||||||
|
self.diff = ms;
|
||||||
|
self.prev = prevTime;
|
||||||
|
self.curr = curr;
|
||||||
|
prevTime = curr;
|
||||||
|
|
||||||
|
args[0] = createDebug.coerce(args[0]);
|
||||||
|
|
||||||
|
if (typeof args[0] !== 'string') {
|
||||||
|
// Anything else let's inspect with %O
|
||||||
|
args.unshift('%O');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply any `formatters` transformations
|
||||||
|
let index = 0;
|
||||||
|
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
|
||||||
|
// If we encounter an escaped % then don't increase the array index
|
||||||
|
if (match === '%%') {
|
||||||
|
return '%';
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
const formatter = createDebug.formatters[format];
|
||||||
|
if (typeof formatter === 'function') {
|
||||||
|
const val = args[index];
|
||||||
|
match = formatter.call(self, val);
|
||||||
|
|
||||||
|
// Now we need to remove `args[index]` since it's inlined in the `format`
|
||||||
|
args.splice(index, 1);
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply env-specific formatting (colors, etc.)
|
||||||
|
createDebug.formatArgs.call(self, args);
|
||||||
|
|
||||||
|
const logFn = self.log || createDebug.log;
|
||||||
|
logFn.apply(self, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.namespace = namespace;
|
||||||
|
debug.useColors = createDebug.useColors();
|
||||||
|
debug.color = createDebug.selectColor(namespace);
|
||||||
|
debug.extend = extend;
|
||||||
|
debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release.
|
||||||
|
|
||||||
|
Object.defineProperty(debug, 'enabled', {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: false,
|
||||||
|
get: () => {
|
||||||
|
if (enableOverride !== null) {
|
||||||
|
return enableOverride;
|
||||||
|
}
|
||||||
|
if (namespacesCache !== createDebug.namespaces) {
|
||||||
|
namespacesCache = createDebug.namespaces;
|
||||||
|
enabledCache = createDebug.enabled(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledCache;
|
||||||
|
},
|
||||||
|
set: v => {
|
||||||
|
enableOverride = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Env-specific initialization logic for debug instances
|
||||||
|
if (typeof createDebug.init === 'function') {
|
||||||
|
createDebug.init(debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extend(namespace, delimiter) {
|
||||||
|
const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace);
|
||||||
|
newDebug.log = this.log;
|
||||||
|
return newDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables a debug mode by namespaces. This can include modes
|
||||||
|
* separated by a colon and wildcards.
|
||||||
|
*
|
||||||
|
* @param {String} namespaces
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
function enable(namespaces) {
|
||||||
|
createDebug.save(namespaces);
|
||||||
|
createDebug.namespaces = namespaces;
|
||||||
|
|
||||||
|
createDebug.names = [];
|
||||||
|
createDebug.skips = [];
|
||||||
|
|
||||||
|
const split = (typeof namespaces === 'string' ? namespaces : '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, ',')
|
||||||
|
.split(',')
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const ns of split) {
|
||||||
|
if (ns[0] === '-') {
|
||||||
|
createDebug.skips.push(ns.slice(1));
|
||||||
|
} else {
|
||||||
|
createDebug.names.push(ns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given string matches a namespace template, honoring
|
||||||
|
* asterisks as wildcards.
|
||||||
|
*
|
||||||
|
* @param {String} search
|
||||||
|
* @param {String} template
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
function matchesTemplate(search, template) {
|
||||||
|
let searchIndex = 0;
|
||||||
|
let templateIndex = 0;
|
||||||
|
let starIndex = -1;
|
||||||
|
let matchIndex = 0;
|
||||||
|
|
||||||
|
while (searchIndex < search.length) {
|
||||||
|
if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === '*')) {
|
||||||
|
// Match character or proceed with wildcard
|
||||||
|
if (template[templateIndex] === '*') {
|
||||||
|
starIndex = templateIndex;
|
||||||
|
matchIndex = searchIndex;
|
||||||
|
templateIndex++; // Skip the '*'
|
||||||
|
} else {
|
||||||
|
searchIndex++;
|
||||||
|
templateIndex++;
|
||||||
|
}
|
||||||
|
} else if (starIndex !== -1) { // eslint-disable-line no-negated-condition
|
||||||
|
// Backtrack to the last '*' and try to match more characters
|
||||||
|
templateIndex = starIndex + 1;
|
||||||
|
matchIndex++;
|
||||||
|
searchIndex = matchIndex;
|
||||||
|
} else {
|
||||||
|
return false; // No match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing '*' in template
|
||||||
|
while (templateIndex < template.length && template[templateIndex] === '*') {
|
||||||
|
templateIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templateIndex === template.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable debug output.
|
||||||
|
*
|
||||||
|
* @return {String} namespaces
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
function disable() {
|
||||||
|
const namespaces = [
|
||||||
|
...createDebug.names,
|
||||||
|
...createDebug.skips.map(namespace => '-' + namespace)
|
||||||
|
].join(',');
|
||||||
|
createDebug.enable('');
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given mode name is enabled, false otherwise.
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @return {Boolean}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
function enabled(name) {
|
||||||
|
for (const skip of createDebug.skips) {
|
||||||
|
if (matchesTemplate(name, skip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ns of createDebug.names) {
|
||||||
|
if (matchesTemplate(name, ns)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce `val`.
|
||||||
|
*
|
||||||
|
* @param {Mixed} val
|
||||||
|
* @return {Mixed}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
function coerce(val) {
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return val.stack || val.message;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XXX DO NOT USE. This is a temporary stub function.
|
||||||
|
* XXX It WILL be removed in the next major release.
|
||||||
|
*/
|
||||||
|
function destroy() {
|
||||||
|
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
createDebug.enable(createDebug.load());
|
||||||
|
|
||||||
|
return createDebug;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = setup;
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Detect Electron renderer / nwjs process, which is node, but we should
|
||||||
|
* treat as a browser.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) {
|
||||||
|
module.exports = require('./browser.js');
|
||||||
|
} else {
|
||||||
|
module.exports = require('./node.js');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const tty = require('tty');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the Node.js implementation of `debug()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.init = init;
|
||||||
|
exports.log = log;
|
||||||
|
exports.formatArgs = formatArgs;
|
||||||
|
exports.save = save;
|
||||||
|
exports.load = load;
|
||||||
|
exports.useColors = useColors;
|
||||||
|
exports.destroy = util.deprecate(
|
||||||
|
() => {},
|
||||||
|
'Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.colors = [6, 2, 3, 4, 5, 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Optional dependency (as in, doesn't need to be installed, NOT like optionalDependencies in package.json)
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
const supportsColor = require('supports-color');
|
||||||
|
|
||||||
|
if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
|
||||||
|
exports.colors = [
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
38,
|
||||||
|
39,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
56,
|
||||||
|
57,
|
||||||
|
62,
|
||||||
|
63,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
76,
|
||||||
|
77,
|
||||||
|
78,
|
||||||
|
79,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
92,
|
||||||
|
93,
|
||||||
|
98,
|
||||||
|
99,
|
||||||
|
112,
|
||||||
|
113,
|
||||||
|
128,
|
||||||
|
129,
|
||||||
|
134,
|
||||||
|
135,
|
||||||
|
148,
|
||||||
|
149,
|
||||||
|
160,
|
||||||
|
161,
|
||||||
|
162,
|
||||||
|
163,
|
||||||
|
164,
|
||||||
|
165,
|
||||||
|
166,
|
||||||
|
167,
|
||||||
|
168,
|
||||||
|
169,
|
||||||
|
170,
|
||||||
|
171,
|
||||||
|
172,
|
||||||
|
173,
|
||||||
|
178,
|
||||||
|
179,
|
||||||
|
184,
|
||||||
|
185,
|
||||||
|
196,
|
||||||
|
197,
|
||||||
|
198,
|
||||||
|
199,
|
||||||
|
200,
|
||||||
|
201,
|
||||||
|
202,
|
||||||
|
203,
|
||||||
|
204,
|
||||||
|
205,
|
||||||
|
206,
|
||||||
|
207,
|
||||||
|
208,
|
||||||
|
209,
|
||||||
|
214,
|
||||||
|
215,
|
||||||
|
220,
|
||||||
|
221
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Swallow - we only care if `supports-color` is available; it doesn't have to be.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build up the default `inspectOpts` object from the environment variables.
|
||||||
|
*
|
||||||
|
* $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.inspectOpts = Object.keys(process.env).filter(key => {
|
||||||
|
return /^debug_/i.test(key);
|
||||||
|
}).reduce((obj, key) => {
|
||||||
|
// Camel-case
|
||||||
|
const prop = key
|
||||||
|
.substring(6)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_([a-z])/g, (_, k) => {
|
||||||
|
return k.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Coerce string value into JS value
|
||||||
|
let val = process.env[key];
|
||||||
|
if (/^(yes|on|true|enabled)$/i.test(val)) {
|
||||||
|
val = true;
|
||||||
|
} else if (/^(no|off|false|disabled)$/i.test(val)) {
|
||||||
|
val = false;
|
||||||
|
} else if (val === 'null') {
|
||||||
|
val = null;
|
||||||
|
} else {
|
||||||
|
val = Number(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[prop] = val;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is stdout a TTY? Colored output is enabled when `true`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function useColors() {
|
||||||
|
return 'colors' in exports.inspectOpts ?
|
||||||
|
Boolean(exports.inspectOpts.colors) :
|
||||||
|
tty.isatty(process.stderr.fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds ANSI color escape codes if enabled.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatArgs(args) {
|
||||||
|
const {namespace: name, useColors} = this;
|
||||||
|
|
||||||
|
if (useColors) {
|
||||||
|
const c = this.color;
|
||||||
|
const colorCode = '\u001B[3' + (c < 8 ? c : '8;5;' + c);
|
||||||
|
const prefix = ` ${colorCode};1m${name} \u001B[0m`;
|
||||||
|
|
||||||
|
args[0] = prefix + args[0].split('\n').join('\n' + prefix);
|
||||||
|
args.push(colorCode + 'm+' + module.exports.humanize(this.diff) + '\u001B[0m');
|
||||||
|
} else {
|
||||||
|
args[0] = getDate() + name + ' ' + args[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDate() {
|
||||||
|
if (exports.inspectOpts.hideDate) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return new Date().toISOString() + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes `util.formatWithOptions()` with the specified arguments and writes to stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function log(...args) {
|
||||||
|
return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save `namespaces`.
|
||||||
|
*
|
||||||
|
* @param {String} namespaces
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
function save(namespaces) {
|
||||||
|
if (namespaces) {
|
||||||
|
process.env.DEBUG = namespaces;
|
||||||
|
} else {
|
||||||
|
// If you set a process.env field to null or undefined, it gets cast to the
|
||||||
|
// string 'null' or 'undefined'. Just delete instead.
|
||||||
|
delete process.env.DEBUG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load `namespaces`.
|
||||||
|
*
|
||||||
|
* @return {String} returns the previously persisted debug modes
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
return process.env.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init logic for `debug` instances.
|
||||||
|
*
|
||||||
|
* Create a new `inspectOpts` object in case `useColors` is set
|
||||||
|
* differently for a particular `debug` instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function init(debug) {
|
||||||
|
debug.inspectOpts = {};
|
||||||
|
|
||||||
|
const keys = Object.keys(exports.inspectOpts);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = require('./common')(exports);
|
||||||
|
|
||||||
|
const {formatters} = module.exports;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map %o to `util.inspect()`, all on a single line.
|
||||||
|
*/
|
||||||
|
|
||||||
|
formatters.o = function (v) {
|
||||||
|
this.inspectOpts.colors = this.useColors;
|
||||||
|
return util.inspect(v, this.inspectOpts)
|
||||||
|
.split('\n')
|
||||||
|
.map(str => str.trim())
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map %O to `util.inspect()`, allowing multiple lines if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
formatters.O = function (v) {
|
||||||
|
this.inspectOpts.colors = this.useColors;
|
||||||
|
return util.inspect(v, this.inspectOpts);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
2.0.0 / 2018-10-26
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Drop support for Node.js 0.6
|
||||||
|
* Replace internal `eval` usage with `Function` constructor
|
||||||
|
* Use instance methods on `process` to check for listeners
|
||||||
|
|
||||||
|
1.1.2 / 2018-01-11
|
||||||
|
==================
|
||||||
|
|
||||||
|
* perf: remove argument reassignment
|
||||||
|
* Support Node.js 0.6 to 9.x
|
||||||
|
|
||||||
|
1.1.1 / 2017-07-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Remove unnecessary `Buffer` loading
|
||||||
|
* Support Node.js 0.6 to 8.x
|
||||||
|
|
||||||
|
1.1.0 / 2015-09-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Enable strict mode in more places
|
||||||
|
* Support io.js 3.x
|
||||||
|
* Support io.js 2.x
|
||||||
|
* Support web browser loading
|
||||||
|
- Requires bundler like Browserify or webpack
|
||||||
|
|
||||||
|
1.0.1 / 2015-04-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix `TypeError`s when under `'use strict'` code
|
||||||
|
* Fix useless type name on auto-generated messages
|
||||||
|
* Support io.js 1.x
|
||||||
|
* Support Node.js 0.12
|
||||||
|
|
||||||
|
1.0.0 / 2014-09-17
|
||||||
|
==================
|
||||||
|
|
||||||
|
* No changes
|
||||||
|
|
||||||
|
0.4.5 / 2014-09-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Improve call speed to functions using the function wrapper
|
||||||
|
* Support Node.js 0.6
|
||||||
|
|
||||||
|
0.4.4 / 2014-07-27
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Work-around v8 generating empty stack traces
|
||||||
|
|
||||||
|
0.4.3 / 2014-07-26
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix exception when global `Error.stackTraceLimit` is too low
|
||||||
|
|
||||||
|
0.4.2 / 2014-07-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Correct call site for wrapped functions and properties
|
||||||
|
|
||||||
|
0.4.1 / 2014-07-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Improve automatic message generation for function properties
|
||||||
|
|
||||||
|
0.4.0 / 2014-07-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add `TRACE_DEPRECATION` environment variable
|
||||||
|
* Remove non-standard grey color from color output
|
||||||
|
* Support `--no-deprecation` argument
|
||||||
|
* Support `--trace-deprecation` argument
|
||||||
|
* Support `deprecate.property(fn, prop, message)`
|
||||||
|
|
||||||
|
0.3.0 / 2014-06-16
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add `NO_DEPRECATION` environment variable
|
||||||
|
|
||||||
|
0.2.0 / 2014-06-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add `deprecate.property(obj, prop, message)`
|
||||||
|
* Remove `supports-color` dependency for node.js 0.8
|
||||||
|
|
||||||
|
0.1.0 / 2014-06-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add `deprecate.function(fn, message)`
|
||||||
|
* Add `process.on('deprecation', fn)` emitter
|
||||||
|
* Automatically generate message when omitted from `deprecate()`
|
||||||
|
|
||||||
|
0.0.1 / 2014-06-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix warning for dynamic calls at singe call site
|
||||||
|
|
||||||
|
0.0.0 / 2014-06-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Initial implementation
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014-2018 Douglas Christopher Wilson
|
||||||
|
|
||||||
|
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,280 @@
|
||||||
|
# depd
|
||||||
|
|
||||||
|
[![NPM Version][npm-version-image]][npm-url]
|
||||||
|
[![NPM Downloads][npm-downloads-image]][npm-url]
|
||||||
|
[![Node.js Version][node-image]][node-url]
|
||||||
|
[![Linux Build][travis-image]][travis-url]
|
||||||
|
[![Windows Build][appveyor-image]][appveyor-url]
|
||||||
|
[![Coverage Status][coveralls-image]][coveralls-url]
|
||||||
|
|
||||||
|
Deprecate all the things
|
||||||
|
|
||||||
|
> With great modules comes great responsibility; mark things deprecated!
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
This module is installed directly using `npm`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm install depd
|
||||||
|
```
|
||||||
|
|
||||||
|
This module can also be bundled with systems like
|
||||||
|
[Browserify](http://browserify.org/) or [webpack](https://webpack.github.io/),
|
||||||
|
though by default this module will alter it's API to no longer display or
|
||||||
|
track deprecations.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
<!-- eslint-disable no-unused-vars -->
|
||||||
|
|
||||||
|
```js
|
||||||
|
var deprecate = require('depd')('my-module')
|
||||||
|
```
|
||||||
|
|
||||||
|
This library allows you to display deprecation messages to your users.
|
||||||
|
This library goes above and beyond with deprecation warnings by
|
||||||
|
introspection of the call stack (but only the bits that it is interested
|
||||||
|
in).
|
||||||
|
|
||||||
|
Instead of just warning on the first invocation of a deprecated
|
||||||
|
function and never again, this module will warn on the first invocation
|
||||||
|
of a deprecated function per unique call site, making it ideal to alert
|
||||||
|
users of all deprecated uses across the code base, rather than just
|
||||||
|
whatever happens to execute first.
|
||||||
|
|
||||||
|
The deprecation warnings from this module also include the file and line
|
||||||
|
information for the call into the module that the deprecated function was
|
||||||
|
in.
|
||||||
|
|
||||||
|
**NOTE** this library has a similar interface to the `debug` module, and
|
||||||
|
this module uses the calling file to get the boundary for the call stacks,
|
||||||
|
so you should always create a new `deprecate` object in each file and not
|
||||||
|
within some central file.
|
||||||
|
|
||||||
|
### depd(namespace)
|
||||||
|
|
||||||
|
Create a new deprecate function that uses the given namespace name in the
|
||||||
|
messages and will display the call site prior to the stack entering the
|
||||||
|
file this function was called from. It is highly suggested you use the
|
||||||
|
name of your module as the namespace.
|
||||||
|
|
||||||
|
### deprecate(message)
|
||||||
|
|
||||||
|
Call this function from deprecated code to display a deprecation message.
|
||||||
|
This message will appear once per unique caller site. Caller site is the
|
||||||
|
first call site in the stack in a different file from the caller of this
|
||||||
|
function.
|
||||||
|
|
||||||
|
If the message is omitted, a message is generated for you based on the site
|
||||||
|
of the `deprecate()` call and will display the name of the function called,
|
||||||
|
similar to the name displayed in a stack trace.
|
||||||
|
|
||||||
|
### deprecate.function(fn, message)
|
||||||
|
|
||||||
|
Call this function to wrap a given function in a deprecation message on any
|
||||||
|
call to the function. An optional message can be supplied to provide a custom
|
||||||
|
message.
|
||||||
|
|
||||||
|
### deprecate.property(obj, prop, message)
|
||||||
|
|
||||||
|
Call this function to wrap a given property on object in a deprecation message
|
||||||
|
on any accessing or setting of the property. An optional message can be supplied
|
||||||
|
to provide a custom message.
|
||||||
|
|
||||||
|
The method must be called on the object where the property belongs (not
|
||||||
|
inherited from the prototype).
|
||||||
|
|
||||||
|
If the property is a data descriptor, it will be converted to an accessor
|
||||||
|
descriptor in order to display the deprecation message.
|
||||||
|
|
||||||
|
### process.on('deprecation', fn)
|
||||||
|
|
||||||
|
This module will allow easy capturing of deprecation errors by emitting the
|
||||||
|
errors as the type "deprecation" on the global `process`. If there are no
|
||||||
|
listeners for this type, the errors are written to STDERR as normal, but if
|
||||||
|
there are any listeners, nothing will be written to STDERR and instead only
|
||||||
|
emitted. From there, you can write the errors in a different format or to a
|
||||||
|
logging source.
|
||||||
|
|
||||||
|
The error represents the deprecation and is emitted only once with the same
|
||||||
|
rules as writing to STDERR. The error has the following properties:
|
||||||
|
|
||||||
|
- `message` - This is the message given by the library
|
||||||
|
- `name` - This is always `'DeprecationError'`
|
||||||
|
- `namespace` - This is the namespace the deprecation came from
|
||||||
|
- `stack` - This is the stack of the call to the deprecated thing
|
||||||
|
|
||||||
|
Example `error.stack` output:
|
||||||
|
|
||||||
|
```
|
||||||
|
DeprecationError: my-cool-module deprecated oldfunction
|
||||||
|
at Object.<anonymous> ([eval]-wrapper:6:22)
|
||||||
|
at Module._compile (module.js:456:26)
|
||||||
|
at evalScript (node.js:532:25)
|
||||||
|
at startup (node.js:80:7)
|
||||||
|
at node.js:902:3
|
||||||
|
```
|
||||||
|
|
||||||
|
### process.env.NO_DEPRECATION
|
||||||
|
|
||||||
|
As a user of modules that are deprecated, the environment variable `NO_DEPRECATION`
|
||||||
|
is provided as a quick solution to silencing deprecation warnings from being
|
||||||
|
output. The format of this is similar to that of `DEBUG`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ NO_DEPRECATION=my-module,othermod node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
This will suppress deprecations from being output for "my-module" and "othermod".
|
||||||
|
The value is a list of comma-separated namespaces. To suppress every warning
|
||||||
|
across all namespaces, use the value `*` for a namespace.
|
||||||
|
|
||||||
|
Providing the argument `--no-deprecation` to the `node` executable will suppress
|
||||||
|
all deprecations (only available in Node.js 0.8 or higher).
|
||||||
|
|
||||||
|
**NOTE** This will not suppress the deperecations given to any "deprecation"
|
||||||
|
event listeners, just the output to STDERR.
|
||||||
|
|
||||||
|
### process.env.TRACE_DEPRECATION
|
||||||
|
|
||||||
|
As a user of modules that are deprecated, the environment variable `TRACE_DEPRECATION`
|
||||||
|
is provided as a solution to getting more detailed location information in deprecation
|
||||||
|
warnings by including the entire stack trace. The format of this is the same as
|
||||||
|
`NO_DEPRECATION`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ TRACE_DEPRECATION=my-module,othermod node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
This will include stack traces for deprecations being output for "my-module" and
|
||||||
|
"othermod". The value is a list of comma-separated namespaces. To trace every
|
||||||
|
warning across all namespaces, use the value `*` for a namespace.
|
||||||
|
|
||||||
|
Providing the argument `--trace-deprecation` to the `node` executable will trace
|
||||||
|
all deprecations (only available in Node.js 0.8 or higher).
|
||||||
|
|
||||||
|
**NOTE** This will not trace the deperecations silenced by `NO_DEPRECATION`.
|
||||||
|
|
||||||
|
## Display
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When a user calls a function in your library that you mark deprecated, they
|
||||||
|
will see the following written to STDERR (in the given colors, similar colors
|
||||||
|
and layout to the `debug` module):
|
||||||
|
|
||||||
|
```
|
||||||
|
bright cyan bright yellow
|
||||||
|
| | reset cyan
|
||||||
|
| | | |
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
my-cool-module deprecated oldfunction [eval]-wrapper:6:22
|
||||||
|
▲ ▲ ▲ ▲
|
||||||
|
| | | |
|
||||||
|
namespace | | location of mycoolmod.oldfunction() call
|
||||||
|
| deprecation message
|
||||||
|
the word "deprecated"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user redirects their STDERR to a file or somewhere that does not support
|
||||||
|
colors, they see (similar layout to the `debug` module):
|
||||||
|
|
||||||
|
```
|
||||||
|
Sun, 15 Jun 2014 05:21:37 GMT my-cool-module deprecated oldfunction at [eval]-wrapper:6:22
|
||||||
|
▲ ▲ ▲ ▲ ▲
|
||||||
|
| | | | |
|
||||||
|
timestamp of message namespace | | location of mycoolmod.oldfunction() call
|
||||||
|
| deprecation message
|
||||||
|
the word "deprecated"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Deprecating all calls to a function
|
||||||
|
|
||||||
|
This will display a deprecated message about "oldfunction" being deprecated
|
||||||
|
from "my-module" on STDERR.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var deprecate = require('depd')('my-cool-module')
|
||||||
|
|
||||||
|
// message automatically derived from function name
|
||||||
|
// Object.oldfunction
|
||||||
|
exports.oldfunction = deprecate.function(function oldfunction () {
|
||||||
|
// all calls to function are deprecated
|
||||||
|
})
|
||||||
|
|
||||||
|
// specific message
|
||||||
|
exports.oldfunction = deprecate.function(function () {
|
||||||
|
// all calls to function are deprecated
|
||||||
|
}, 'oldfunction')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditionally deprecating a function call
|
||||||
|
|
||||||
|
This will display a deprecated message about "weirdfunction" being deprecated
|
||||||
|
from "my-module" on STDERR when called with less than 2 arguments.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var deprecate = require('depd')('my-cool-module')
|
||||||
|
|
||||||
|
exports.weirdfunction = function () {
|
||||||
|
if (arguments.length < 2) {
|
||||||
|
// calls with 0 or 1 args are deprecated
|
||||||
|
deprecate('weirdfunction args < 2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When calling `deprecate` as a function, the warning is counted per call site
|
||||||
|
within your own module, so you can display different deprecations depending
|
||||||
|
on different situations and the users will still get all the warnings:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var deprecate = require('depd')('my-cool-module')
|
||||||
|
|
||||||
|
exports.weirdfunction = function () {
|
||||||
|
if (arguments.length < 2) {
|
||||||
|
// calls with 0 or 1 args are deprecated
|
||||||
|
deprecate('weirdfunction args < 2')
|
||||||
|
} else if (typeof arguments[0] !== 'string') {
|
||||||
|
// calls with non-string first argument are deprecated
|
||||||
|
deprecate('weirdfunction non-string first arg')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecating property access
|
||||||
|
|
||||||
|
This will display a deprecated message about "oldprop" being deprecated
|
||||||
|
from "my-module" on STDERR when accessed. A deprecation will be displayed
|
||||||
|
when setting the value and when getting the value.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var deprecate = require('depd')('my-cool-module')
|
||||||
|
|
||||||
|
exports.oldprop = 'something'
|
||||||
|
|
||||||
|
// message automatically derives from property name
|
||||||
|
deprecate.property(exports, 'oldprop')
|
||||||
|
|
||||||
|
// explicit message
|
||||||
|
deprecate.property(exports, 'oldprop', 'oldprop >= 0.10')
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/nodejs-depd/master?label=windows
|
||||||
|
[appveyor-url]: https://ci.appveyor.com/project/dougwilson/nodejs-depd
|
||||||
|
[coveralls-image]: https://badgen.net/coveralls/c/github/dougwilson/nodejs-depd/master
|
||||||
|
[coveralls-url]: https://coveralls.io/r/dougwilson/nodejs-depd?branch=master
|
||||||
|
[node-image]: https://badgen.net/npm/node/depd
|
||||||
|
[node-url]: https://nodejs.org/en/download/
|
||||||
|
[npm-downloads-image]: https://badgen.net/npm/dm/depd
|
||||||
|
[npm-url]: https://npmjs.org/package/depd
|
||||||
|
[npm-version-image]: https://badgen.net/npm/v/depd
|
||||||
|
[travis-image]: https://badgen.net/travis/dougwilson/nodejs-depd/master?label=linux
|
||||||
|
[travis-url]: https://travis-ci.org/dougwilson/nodejs-depd
|
||||||
|
|
@ -0,0 +1,538 @@
|
||||||
|
/*!
|
||||||
|
* depd
|
||||||
|
* Copyright(c) 2014-2018 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var relative = require('path').relative
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = depd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to base files on.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var basePath = process.cwd()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if namespace is contained in the string.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function containsNamespace (str, namespace) {
|
||||||
|
var vals = str.split(/[ ,]+/)
|
||||||
|
var ns = String(namespace).toLowerCase()
|
||||||
|
|
||||||
|
for (var i = 0; i < vals.length; i++) {
|
||||||
|
var val = vals[i]
|
||||||
|
|
||||||
|
// namespace contained
|
||||||
|
if (val && (val === '*' || val.toLowerCase() === ns)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a data descriptor to accessor descriptor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function convertDataDescriptorToAccessor (obj, prop, message) {
|
||||||
|
var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
|
||||||
|
var value = descriptor.value
|
||||||
|
|
||||||
|
descriptor.get = function getter () { return value }
|
||||||
|
|
||||||
|
if (descriptor.writable) {
|
||||||
|
descriptor.set = function setter (val) { return (value = val) }
|
||||||
|
}
|
||||||
|
|
||||||
|
delete descriptor.value
|
||||||
|
delete descriptor.writable
|
||||||
|
|
||||||
|
Object.defineProperty(obj, prop, descriptor)
|
||||||
|
|
||||||
|
return descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create arguments string to keep arity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createArgumentsString (arity) {
|
||||||
|
var str = ''
|
||||||
|
|
||||||
|
for (var i = 0; i < arity; i++) {
|
||||||
|
str += ', arg' + i
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.substr(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create stack string from stack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createStackString (stack) {
|
||||||
|
var str = this.name + ': ' + this.namespace
|
||||||
|
|
||||||
|
if (this.message) {
|
||||||
|
str += ' deprecated ' + this.message
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < stack.length; i++) {
|
||||||
|
str += '\n at ' + stack[i].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create deprecate for namespace in caller.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function depd (namespace) {
|
||||||
|
if (!namespace) {
|
||||||
|
throw new TypeError('argument namespace is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
var stack = getStack()
|
||||||
|
var site = callSiteLocation(stack[1])
|
||||||
|
var file = site[0]
|
||||||
|
|
||||||
|
function deprecate (message) {
|
||||||
|
// call to self as log
|
||||||
|
log.call(deprecate, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecate._file = file
|
||||||
|
deprecate._ignored = isignored(namespace)
|
||||||
|
deprecate._namespace = namespace
|
||||||
|
deprecate._traced = istraced(namespace)
|
||||||
|
deprecate._warned = Object.create(null)
|
||||||
|
|
||||||
|
deprecate.function = wrapfunction
|
||||||
|
deprecate.property = wrapproperty
|
||||||
|
|
||||||
|
return deprecate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if event emitter has listeners of a given type.
|
||||||
|
*
|
||||||
|
* The way to do this check is done three different ways in Node.js >= 0.8
|
||||||
|
* so this consolidates them into a minimal set using instance methods.
|
||||||
|
*
|
||||||
|
* @param {EventEmitter} emitter
|
||||||
|
* @param {string} type
|
||||||
|
* @returns {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function eehaslisteners (emitter, type) {
|
||||||
|
var count = typeof emitter.listenerCount !== 'function'
|
||||||
|
? emitter.listeners(type).length
|
||||||
|
: emitter.listenerCount(type)
|
||||||
|
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if namespace is ignored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isignored (namespace) {
|
||||||
|
if (process.noDeprecation) {
|
||||||
|
// --no-deprecation support
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var str = process.env.NO_DEPRECATION || ''
|
||||||
|
|
||||||
|
// namespace ignored
|
||||||
|
return containsNamespace(str, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if namespace is traced.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function istraced (namespace) {
|
||||||
|
if (process.traceDeprecation) {
|
||||||
|
// --trace-deprecation support
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var str = process.env.TRACE_DEPRECATION || ''
|
||||||
|
|
||||||
|
// namespace traced
|
||||||
|
return containsNamespace(str, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display deprecation message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function log (message, site) {
|
||||||
|
var haslisteners = eehaslisteners(process, 'deprecation')
|
||||||
|
|
||||||
|
// abort early if no destination
|
||||||
|
if (!haslisteners && this._ignored) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var caller
|
||||||
|
var callFile
|
||||||
|
var callSite
|
||||||
|
var depSite
|
||||||
|
var i = 0
|
||||||
|
var seen = false
|
||||||
|
var stack = getStack()
|
||||||
|
var file = this._file
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
// provided site
|
||||||
|
depSite = site
|
||||||
|
callSite = callSiteLocation(stack[1])
|
||||||
|
callSite.name = depSite.name
|
||||||
|
file = callSite[0]
|
||||||
|
} else {
|
||||||
|
// get call site
|
||||||
|
i = 2
|
||||||
|
depSite = callSiteLocation(stack[i])
|
||||||
|
callSite = depSite
|
||||||
|
}
|
||||||
|
|
||||||
|
// get caller of deprecated thing in relation to file
|
||||||
|
for (; i < stack.length; i++) {
|
||||||
|
caller = callSiteLocation(stack[i])
|
||||||
|
callFile = caller[0]
|
||||||
|
|
||||||
|
if (callFile === file) {
|
||||||
|
seen = true
|
||||||
|
} else if (callFile === this._file) {
|
||||||
|
file = this._file
|
||||||
|
} else if (seen) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = caller
|
||||||
|
? depSite.join(':') + '__' + caller.join(':')
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (key !== undefined && key in this._warned) {
|
||||||
|
// already warned
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._warned[key] = true
|
||||||
|
|
||||||
|
// generate automatic message from call site
|
||||||
|
var msg = message
|
||||||
|
if (!msg) {
|
||||||
|
msg = callSite === depSite || !callSite.name
|
||||||
|
? defaultMessage(depSite)
|
||||||
|
: defaultMessage(callSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit deprecation if listeners exist
|
||||||
|
if (haslisteners) {
|
||||||
|
var err = DeprecationError(this._namespace, msg, stack.slice(i))
|
||||||
|
process.emit('deprecation', err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// format and write message
|
||||||
|
var format = process.stderr.isTTY
|
||||||
|
? formatColor
|
||||||
|
: formatPlain
|
||||||
|
var output = format.call(this, msg, caller, stack.slice(i))
|
||||||
|
process.stderr.write(output + '\n', 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get call site location as array.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function callSiteLocation (callSite) {
|
||||||
|
var file = callSite.getFileName() || '<anonymous>'
|
||||||
|
var line = callSite.getLineNumber()
|
||||||
|
var colm = callSite.getColumnNumber()
|
||||||
|
|
||||||
|
if (callSite.isEval()) {
|
||||||
|
file = callSite.getEvalOrigin() + ', ' + file
|
||||||
|
}
|
||||||
|
|
||||||
|
var site = [file, line, colm]
|
||||||
|
|
||||||
|
site.callSite = callSite
|
||||||
|
site.name = callSite.getFunctionName()
|
||||||
|
|
||||||
|
return site
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a default message from the site.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function defaultMessage (site) {
|
||||||
|
var callSite = site.callSite
|
||||||
|
var funcName = site.name
|
||||||
|
|
||||||
|
// make useful anonymous name
|
||||||
|
if (!funcName) {
|
||||||
|
funcName = '<anonymous@' + formatLocation(site) + '>'
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = callSite.getThis()
|
||||||
|
var typeName = context && callSite.getTypeName()
|
||||||
|
|
||||||
|
// ignore useless type name
|
||||||
|
if (typeName === 'Object') {
|
||||||
|
typeName = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// make useful type name
|
||||||
|
if (typeName === 'Function') {
|
||||||
|
typeName = context.name || typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeName && callSite.getMethodName()
|
||||||
|
? typeName + '.' + funcName
|
||||||
|
: funcName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format deprecation message without color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatPlain (msg, caller, stack) {
|
||||||
|
var timestamp = new Date().toUTCString()
|
||||||
|
|
||||||
|
var formatted = timestamp +
|
||||||
|
' ' + this._namespace +
|
||||||
|
' deprecated ' + msg
|
||||||
|
|
||||||
|
// add stack trace
|
||||||
|
if (this._traced) {
|
||||||
|
for (var i = 0; i < stack.length; i++) {
|
||||||
|
formatted += '\n at ' + stack[i].toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caller) {
|
||||||
|
formatted += ' at ' + formatLocation(caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format deprecation message with color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatColor (msg, caller, stack) {
|
||||||
|
var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan
|
||||||
|
' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow
|
||||||
|
' \x1b[0m' + msg + '\x1b[39m' // reset
|
||||||
|
|
||||||
|
// add stack trace
|
||||||
|
if (this._traced) {
|
||||||
|
for (var i = 0; i < stack.length; i++) {
|
||||||
|
formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caller) {
|
||||||
|
formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format call site location.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function formatLocation (callSite) {
|
||||||
|
return relative(basePath, callSite[0]) +
|
||||||
|
':' + callSite[1] +
|
||||||
|
':' + callSite[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stack as array of call sites.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getStack () {
|
||||||
|
var limit = Error.stackTraceLimit
|
||||||
|
var obj = {}
|
||||||
|
var prep = Error.prepareStackTrace
|
||||||
|
|
||||||
|
Error.prepareStackTrace = prepareObjectStackTrace
|
||||||
|
Error.stackTraceLimit = Math.max(10, limit)
|
||||||
|
|
||||||
|
// capture the stack
|
||||||
|
Error.captureStackTrace(obj)
|
||||||
|
|
||||||
|
// slice this function off the top
|
||||||
|
var stack = obj.stack.slice(1)
|
||||||
|
|
||||||
|
Error.prepareStackTrace = prep
|
||||||
|
Error.stackTraceLimit = limit
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture call site stack from v8.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function prepareObjectStackTrace (obj, stack) {
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a wrapped function in a deprecation message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function wrapfunction (fn, message) {
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
throw new TypeError('argument fn must be a function')
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = createArgumentsString(fn.length)
|
||||||
|
var stack = getStack()
|
||||||
|
var site = callSiteLocation(stack[1])
|
||||||
|
|
||||||
|
site.name = fn.name
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
|
||||||
|
'"use strict"\n' +
|
||||||
|
'return function (' + args + ') {' +
|
||||||
|
'log.call(deprecate, message, site)\n' +
|
||||||
|
'return fn.apply(this, arguments)\n' +
|
||||||
|
'}')(fn, log, this, message, site)
|
||||||
|
|
||||||
|
return deprecatedfn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap property in a deprecation message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function wrapproperty (obj, prop, message) {
|
||||||
|
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
||||||
|
throw new TypeError('argument obj must be object')
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
|
||||||
|
|
||||||
|
if (!descriptor) {
|
||||||
|
throw new TypeError('must call property on owner object')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descriptor.configurable) {
|
||||||
|
throw new TypeError('property must be configurable')
|
||||||
|
}
|
||||||
|
|
||||||
|
var deprecate = this
|
||||||
|
var stack = getStack()
|
||||||
|
var site = callSiteLocation(stack[1])
|
||||||
|
|
||||||
|
// set site name
|
||||||
|
site.name = prop
|
||||||
|
|
||||||
|
// convert data descriptor
|
||||||
|
if ('value' in descriptor) {
|
||||||
|
descriptor = convertDataDescriptorToAccessor(obj, prop, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var get = descriptor.get
|
||||||
|
var set = descriptor.set
|
||||||
|
|
||||||
|
// wrap getter
|
||||||
|
if (typeof get === 'function') {
|
||||||
|
descriptor.get = function getter () {
|
||||||
|
log.call(deprecate, message, site)
|
||||||
|
return get.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap setter
|
||||||
|
if (typeof set === 'function') {
|
||||||
|
descriptor.set = function setter () {
|
||||||
|
log.call(deprecate, message, site)
|
||||||
|
return set.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(obj, prop, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create DeprecationError for deprecation
|
||||||
|
*/
|
||||||
|
|
||||||
|
function DeprecationError (namespace, message, stack) {
|
||||||
|
var error = new Error()
|
||||||
|
var stackString
|
||||||
|
|
||||||
|
Object.defineProperty(error, 'constructor', {
|
||||||
|
value: DeprecationError
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(error, 'message', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
value: message,
|
||||||
|
writable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(error, 'name', {
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
value: 'DeprecationError',
|
||||||
|
writable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(error, 'namespace', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
value: namespace,
|
||||||
|
writable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(error, 'stack', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
get: function () {
|
||||||
|
if (stackString !== undefined) {
|
||||||
|
return stackString
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare stack trace
|
||||||
|
return (stackString = createStackString.call(this, stack))
|
||||||
|
},
|
||||||
|
set: function setter (val) {
|
||||||
|
stackString = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*!
|
||||||
|
* depd
|
||||||
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = depd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create deprecate for namespace in caller.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function depd (namespace) {
|
||||||
|
if (!namespace) {
|
||||||
|
throw new TypeError('argument namespace is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
function deprecate (message) {
|
||||||
|
// no-op in browser
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecate._file = undefined
|
||||||
|
deprecate._ignored = true
|
||||||
|
deprecate._namespace = namespace
|
||||||
|
deprecate._traced = false
|
||||||
|
deprecate._warned = Object.create(null)
|
||||||
|
|
||||||
|
deprecate.function = wrapfunction
|
||||||
|
deprecate.property = wrapproperty
|
||||||
|
|
||||||
|
return deprecate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a wrapped function in a deprecation message.
|
||||||
|
*
|
||||||
|
* This is a no-op version of the wrapper, which does nothing but call
|
||||||
|
* validation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function wrapfunction (fn, message) {
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
throw new TypeError('argument fn must be a function')
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap property in a deprecation message.
|
||||||
|
*
|
||||||
|
* This is a no-op version of the wrapper, which does nothing but call
|
||||||
|
* validation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function wrapproperty (obj, prop, message) {
|
||||||
|
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
|
||||||
|
throw new TypeError('argument obj must be object')
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
|
||||||
|
|
||||||
|
if (!descriptor) {
|
||||||
|
throw new TypeError('must call property on owner object')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descriptor.configurable) {
|
||||||
|
throw new TypeError('property must be configurable')
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue