mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
fix(waf): complete geography match options
Use full country and ISO subdivision data, show localized country names with codes, and add searchable region selection.
This commit is contained in:
@@ -29,6 +29,7 @@ sidebar: false
|
|||||||
### 变更
|
### 变更
|
||||||
|
|
||||||
- 优化了 WAF 规则编辑器的初始视图和操作方式,编辑规则时可看到更多上下文并可直接管理节点、连线和启用状态。
|
- 优化了 WAF 规则编辑器的初始视图和操作方式,编辑规则时可看到更多上下文并可直接管理节点、连线和启用状态。
|
||||||
|
- WAF 地域匹配编辑器改用完整国家与一级行政区数据,国家选项同时显示中文名称和 ISO 代码,行政区支持按名称或代码搜索。
|
||||||
- Agent 现内置国家和城市地址库,首次启动无需下载即可使用地区匹配功能,并会在后续自动更新数据。
|
- Agent 现内置国家和城市地址库,首次启动无需下载即可使用地区匹配功能,并会在后续自动更新数据。
|
||||||
- 默认关闭 Redis maintenance notifications 自动协商,减少不支持该功能的 Redis 服务产生兼容性警告。
|
- 默认关闭 Redis maintenance notifications 自动协商,减少不支持该功能的 Redis 服务产生兼容性警告。
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ React Flow 编辑页采用全宽画布和固定右侧属性栏:
|
|||||||
* “添加处理单元”提供 IP 匹配、地域匹配、PoW 和阻止;开始与通过由默认图提供且不可删除或重复添加。
|
* “添加处理单元”提供 IP 匹配、地域匹配、PoW 和阻止;开始与通过由默认图提供且不可删除或重复添加。
|
||||||
* 选中普通节点或连线后可使用画布删除按钮或 Delete/Backspace 删除;删除节点时同步移除关联连线。
|
* 选中普通节点或连线后可使用画布删除按钮或 Delete/Backspace 删除;删除节点时同步移除关联连线。
|
||||||
* 右侧属性栏默认隐藏,选中节点后才显示并用于编辑配置;点击连线或画布空白处时收起。
|
* 右侧属性栏默认隐藏,选中节点后才显示并用于编辑配置;点击连线或画布空白处时收起。
|
||||||
|
* 地域匹配属性使用完整国家与 ISO 3166-2 一级行政区数据;国家选项同时显示本地化名称与代码,行政区支持按国家名、行政区名或代码搜索,避免一次渲染数千个选项。
|
||||||
* 离开存在未保存变更的页面前必须提示;保存冲突和 Server 校验错误应定位到相关节点或边。
|
* 离开存在未保存变更的页面前必须提示;保存冲突和 Server 校验错误应定位到相关节点或边。
|
||||||
|
|
||||||
WAF 列表展示规则名称、启用状态、节点数量、应用路由数量和更新时间。新建规则的对话框只有名称字段,成功后立即导航到编排页面。
|
WAF 列表展示规则名称、启用状态、节点数量、应用路由数量和更新时间。新建规则的对话框只有名称字段,成功后立即导航到编排页面。
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ OpenFlare WAF 使用可视化有向无环图编排规则。新建规则时只填
|
|||||||
- **通过**:结束当前规则;若路由仍有后续规则则继续执行。
|
- **通过**:结束当前规则;若路由仍有后续规则则继续执行。
|
||||||
- **阻止**:立即按配置的状态码和 HTML 响应终止请求。
|
- **阻止**:立即按配置的状态码和 HTML 响应终止请求。
|
||||||
- **IP 匹配**:配置 IP、CIDR 或 IP 组,分别连接 `true`、`false`。
|
- **IP 匹配**:配置 IP、CIDR 或 IP 组,分别连接 `true`、`false`。
|
||||||
- **地域匹配**:按国家或地区代码分支;Country 与 City MMDB 缺失时由 Agent 从程序内嵌数据库初始化,并按配置周期更新。City MMDB 不可用时按未匹配处理。
|
- **地域匹配**:按国家或 ISO 3166-2 一级行政区代码分支;国家列表同时显示中文名称与代码,行政区可按国家名、行政区名或代码搜索。Country 与 City MMDB 缺失时由 Agent 从程序内嵌数据库初始化,并按配置周期更新。City MMDB 不可用时按未匹配处理。
|
||||||
- **PoW**:未完成挑战时接管请求,验证通过后沿 `next` 继续。
|
- **PoW**:未完成挑战时接管请求,验证通过后沿 `next` 继续。
|
||||||
|
|
||||||
服务端会拒绝循环、悬空出口、不可达节点、重复端口连接和无效配置。保存时携带页面加载得到的 `revision`;发生 409 冲突时应重新加载,避免覆盖他人修改。
|
服务端会拒绝循环、悬空出口、不可达节点、重复端口连接和无效配置。保存时携带页面加载得到的 `revision`;发生 409 冲突时应重新加载,避免覆盖他人修改。
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 实现状态(2026-07-13)
|
## 实现状态(2026-07-13)
|
||||||
|
|
||||||
Tasks 1–11 已实现,包含三段数据库迁移、图模型与编译器、规则 API、发布快照、OpenResty 内存执行器、IP 组协调刷新、React Flow 编辑器、有序绑定、GeoLite2 City/Country 支持以及中文文档与 Swagger 更新。React Flow 画布使用本地受控节点状态处理拖动,并支持显式或键盘删除普通节点与连线。Country 与 City MMDB 均随 Agent 内嵌,缺失文件在启动时从程序内初始化,网络仅用于后续周期更新。
|
Tasks 1–11 已实现,包含三段数据库迁移、图模型与编译器、规则 API、发布快照、OpenResty 内存执行器、IP 组协调刷新、React Flow 编辑器、有序绑定、GeoLite2 City/Country 支持以及中文文档与 Swagger 更新。React Flow 画布使用本地受控节点状态处理拖动,并支持显式或键盘删除普通节点与连线。Country 与 City MMDB 均随 Agent 内嵌,缺失文件在启动时从程序内初始化,网络仅用于后续周期更新。地域属性栏使用完整国家与 ISO 3166-2 一级行政区数据,国家同时展示中文名称与代码,行政区支持按名称或代码搜索。
|
||||||
|
|
||||||
当前工作区已完成 `go test ./...`、前端全量 Vitest(56 项)、`make swagger`、`make code-check` 与 `git diff --check` 验证。Next.js 生产构建在本机持续停留于 Turbopack 的 `Creating an optimized production build ...`,未返回编译错误或成功状态,故不计为通过。
|
当前工作区已完成 `go test ./...`、前端全量 Vitest(56 项)、`make swagger`、`make code-check` 与 `git diff --check` 验证。Next.js 生产构建在本机持续停留于 Turbopack 的 `Creating an optimized production build ...`,未返回编译错误或成功状态,故不计为通过。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { countryOptions, regionOptions } from './geo-options';
|
||||||
|
|
||||||
|
describe('geo options', () => {
|
||||||
|
it('contains the complete country list with localized names', () => {
|
||||||
|
expect(countryOptions).toHaveLength(249);
|
||||||
|
expect(countryOptions).toContainEqual(
|
||||||
|
expect.objectContaining({ value: 'CN', label: '中国' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains searchable ISO subdivision options', () => {
|
||||||
|
expect(regionOptions.length).toBeGreaterThan(4000);
|
||||||
|
expect(regionOptions).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
value: 'CN-BJ',
|
||||||
|
label: expect.stringContaining('Beijing'),
|
||||||
|
searchText: expect.stringContaining('中国'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { allCountries } from 'country-region-data';
|
||||||
|
|
||||||
|
export interface GeoOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCodePattern = /^[A-Z]{2}$/;
|
||||||
|
const regionCodePattern = /^[A-Z0-9]{1,3}$/;
|
||||||
|
const countryDisplayNames = new Intl.DisplayNames(['zh-CN'], {
|
||||||
|
type: 'region',
|
||||||
|
});
|
||||||
|
|
||||||
|
function localizedCountryName(code: string, fallback: string) {
|
||||||
|
const localized = countryDisplayNames.of(code);
|
||||||
|
return !localized || localized === code ? fallback : localized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const countryOptions: GeoOption[] = allCountries
|
||||||
|
.filter(([, code]) => countryCodePattern.test(code))
|
||||||
|
.map(([englishName, code]) => {
|
||||||
|
const label = localizedCountryName(code, englishName);
|
||||||
|
return {
|
||||||
|
value: code,
|
||||||
|
label,
|
||||||
|
searchText: `${label} ${englishName} ${code}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((left, right) => left.label.localeCompare(right.label, 'zh-CN'));
|
||||||
|
|
||||||
|
export const regionOptions: GeoOption[] = allCountries.flatMap(
|
||||||
|
([englishCountryName, countryCode, regions]) => {
|
||||||
|
const countryName = localizedCountryName(countryCode, englishCountryName);
|
||||||
|
return regions.flatMap(([regionName, shortCode]) => {
|
||||||
|
const normalizedShortCode = shortCode?.toUpperCase();
|
||||||
|
if (
|
||||||
|
!normalizedShortCode ||
|
||||||
|
!regionCodePattern.test(normalizedShortCode)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const value = `${countryCode}-${normalizedShortCode}`;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
label: `${regionName} · ${countryName}`,
|
||||||
|
searchText: `${regionName} ${countryName} ${englishCountryName} ${value}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -52,7 +52,7 @@ it('creates any normalized valid geography code', async () => {
|
|||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
render(<NodeProperties node={node} ipGroups={[]} onChange={onChange} />);
|
render(<NodeProperties node={node} ipGroups={[]} onChange={onChange} />);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '国家代码' }));
|
fireEvent.click(screen.getByRole('button', { name: '国家代码' }));
|
||||||
fireEvent.change(await screen.findByPlaceholderText('输入代码并添加'), {
|
fireEvent.change(await screen.findByPlaceholderText('搜索名称或代码'), {
|
||||||
target: { value: 'nz' },
|
target: { value: 'nz' },
|
||||||
});
|
});
|
||||||
fireEvent.click(screen.getByRole('button', { name: '添加代码' }));
|
fireEvent.click(screen.getByRole('button', { name: '添加代码' }));
|
||||||
@@ -62,3 +62,17 @@ it('creates any normalized valid geography code', async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows localized country names together with their codes', async () => {
|
||||||
|
const node: WAFRuleNode = {
|
||||||
|
id: 'geo',
|
||||||
|
type: 'geo_match',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
config: { countries: [], regions: [] },
|
||||||
|
};
|
||||||
|
render(<NodeProperties node={node} ipGroups={[]} onChange={vi.fn()} />);
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '国家代码' }));
|
||||||
|
expect(await screen.findByText('中国')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('CN')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/共 249 个国家\/地区/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|||||||
@@ -28,32 +28,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import type { WAFIPGroup, WAFRuleNode } from '@/lib/services/openflare';
|
import type { WAFIPGroup, WAFRuleNode } from '@/lib/services/openflare';
|
||||||
|
|
||||||
const countries = [
|
import { countryOptions, regionOptions, type GeoOption } from './geo-options';
|
||||||
'CN',
|
|
||||||
'US',
|
|
||||||
'JP',
|
|
||||||
'SG',
|
|
||||||
'DE',
|
|
||||||
'FR',
|
|
||||||
'GB',
|
|
||||||
'CA',
|
|
||||||
'AU',
|
|
||||||
'BR',
|
|
||||||
'IN',
|
|
||||||
'KR',
|
|
||||||
].map((value) => ({ value, label: value }));
|
|
||||||
const regions = [
|
|
||||||
'CN-BJ',
|
|
||||||
'CN-SH',
|
|
||||||
'CN-GD',
|
|
||||||
'CN-ZJ',
|
|
||||||
'US-CA',
|
|
||||||
'US-NY',
|
|
||||||
'US-TX',
|
|
||||||
'JP-13',
|
|
||||||
'DE-BE',
|
|
||||||
'GB-ENG',
|
|
||||||
].map((value) => ({ value, label: value }));
|
|
||||||
|
|
||||||
export function NodeProperties({
|
export function NodeProperties({
|
||||||
node,
|
node,
|
||||||
@@ -119,6 +94,7 @@ function PropertyFields({
|
|||||||
options={ipGroups.map((group) => ({
|
options={ipGroups.map((group) => ({
|
||||||
value: String(group.id),
|
value: String(group.id),
|
||||||
label: group.name,
|
label: group.name,
|
||||||
|
searchText: `${group.name} ${group.id}`,
|
||||||
}))}
|
}))}
|
||||||
value={node.config.ip_group_ids.map(String)}
|
value={node.config.ip_group_ids.map(String)}
|
||||||
onChange={(values) =>
|
onChange={(values) =>
|
||||||
@@ -136,7 +112,8 @@ function PropertyFields({
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
id={`${node.id}-countries`}
|
id={`${node.id}-countries`}
|
||||||
label='国家代码'
|
label='国家代码'
|
||||||
options={countries}
|
description={`共 ${countryOptions.length} 个国家/地区,显示国家名与 ISO 代码`}
|
||||||
|
options={countryOptions}
|
||||||
value={node.config.countries}
|
value={node.config.countries}
|
||||||
creatablePattern={/^[A-Z]{2}$/}
|
creatablePattern={/^[A-Z]{2}$/}
|
||||||
onChange={(countries) =>
|
onChange={(countries) =>
|
||||||
@@ -146,9 +123,11 @@ function PropertyFields({
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
id={`${node.id}-regions`}
|
id={`${node.id}-regions`}
|
||||||
label='地区代码'
|
label='地区代码'
|
||||||
options={regions}
|
description={`共 ${regionOptions.length} 个一级行政区,输入名称或代码搜索`}
|
||||||
|
options={regionOptions}
|
||||||
value={node.config.regions}
|
value={node.config.regions}
|
||||||
creatablePattern={/^[A-Z]{2}-[A-Z0-9]{1,3}$/}
|
creatablePattern={/^[A-Z]{2}-[A-Z0-9]{1,3}$/}
|
||||||
|
searchRequired
|
||||||
onChange={(regions) =>
|
onChange={(regions) =>
|
||||||
onChange({ ...node, config: { ...node.config, regions } })
|
onChange({ ...node, config: { ...node.config, regions } })
|
||||||
}
|
}
|
||||||
@@ -297,28 +276,41 @@ function NumberField({
|
|||||||
function MultiSelect({
|
function MultiSelect({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
|
description,
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
creatablePattern,
|
creatablePattern,
|
||||||
|
searchRequired = false,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
options: { value: string; label: string }[];
|
description?: string;
|
||||||
|
options: GeoOption[];
|
||||||
value: string[];
|
value: string[];
|
||||||
creatablePattern?: RegExp;
|
creatablePattern?: RegExp;
|
||||||
|
searchRequired?: boolean;
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState('');
|
const [draft, setDraft] = useState('');
|
||||||
const normalized = draft.trim().toUpperCase();
|
const normalized = draft.trim().toUpperCase();
|
||||||
const visible = [
|
const query = draft.trim().toLocaleLowerCase();
|
||||||
|
const available = [
|
||||||
...options,
|
...options,
|
||||||
...value
|
...value
|
||||||
.filter(
|
.filter(
|
||||||
(selected) => !options.some((option) => option.value === selected),
|
(selected) => !options.some((option) => option.value === selected),
|
||||||
)
|
)
|
||||||
.map((selected) => ({ value: selected, label: selected })),
|
.map((selected) => ({
|
||||||
|
value: selected,
|
||||||
|
label: selected,
|
||||||
|
searchText: selected,
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
|
const visible = available.filter((option) => {
|
||||||
|
if (!query) return !searchRequired || value.includes(option.value);
|
||||||
|
return option.searchText.toLocaleLowerCase().includes(query);
|
||||||
|
});
|
||||||
const canCreate = Boolean(
|
const canCreate = Boolean(
|
||||||
creatablePattern?.test(normalized) && !value.includes(normalized),
|
creatablePattern?.test(normalized) && !value.includes(normalized),
|
||||||
);
|
);
|
||||||
@@ -331,57 +323,68 @@ function MultiSelect({
|
|||||||
{value.length ? `已选择 ${value.length} 项` : '请选择'}
|
{value.length ? `已选择 ${value.length} 项` : '请选择'}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent align='start' className='flex w-80 flex-col gap-2 p-3'>
|
||||||
align='start'
|
{(creatablePattern || options.length > 0) && (
|
||||||
className='flex max-h-64 flex-col gap-2 overflow-y-auto'
|
|
||||||
>
|
|
||||||
{creatablePattern && (
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Input
|
<Input
|
||||||
aria-label={`新建${label}`}
|
aria-label={`新建${label}`}
|
||||||
placeholder='输入代码并添加'
|
placeholder='搜索名称或代码'
|
||||||
value={draft}
|
value={draft}
|
||||||
onChange={(event) => setDraft(event.target.value)}
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
{creatablePattern && (
|
||||||
size='sm'
|
<Button
|
||||||
disabled={!canCreate}
|
size='sm'
|
||||||
onClick={() => {
|
disabled={!canCreate}
|
||||||
onChange([...value, normalized]);
|
onClick={() => {
|
||||||
setDraft('');
|
onChange([...value, normalized]);
|
||||||
}}
|
setDraft('');
|
||||||
>
|
}}
|
||||||
添加代码
|
>
|
||||||
</Button>
|
添加代码
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{visible.length === 0 ? (
|
<div className='max-h-56 space-y-1 overflow-y-auto pr-1'>
|
||||||
<p className='text-sm text-muted-foreground'>暂无可选项</p>
|
{visible.length === 0 ? (
|
||||||
) : (
|
<p className='px-1 py-3 text-sm text-muted-foreground'>
|
||||||
visible.map((option) => (
|
{searchRequired && !query
|
||||||
<label
|
? `输入名称或代码搜索 ${options.length} 个选项`
|
||||||
key={option.value}
|
: '没有匹配的选项'}
|
||||||
className='flex cursor-pointer items-center gap-2 text-sm'
|
</p>
|
||||||
>
|
) : (
|
||||||
<Checkbox
|
visible.map((option) => (
|
||||||
checked={value.includes(option.value)}
|
<label
|
||||||
onCheckedChange={(checked) =>
|
key={option.value}
|
||||||
onChange(
|
className='flex min-h-8 cursor-pointer items-center gap-2 rounded-md px-1 text-sm hover:bg-accent'
|
||||||
checked
|
>
|
||||||
? [...value, option.value]
|
<Checkbox
|
||||||
: value.filter((item) => item !== option.value),
|
checked={value.includes(option.value)}
|
||||||
)
|
onCheckedChange={(checked) =>
|
||||||
}
|
onChange(
|
||||||
/>
|
checked
|
||||||
<span>{option.label}</span>
|
? [...value, option.value]
|
||||||
<span className='ml-auto font-mono text-xs text-muted-foreground'>
|
: value.filter((item) => item !== option.value),
|
||||||
{option.value}
|
)
|
||||||
</span>
|
}
|
||||||
</label>
|
/>
|
||||||
))
|
<span
|
||||||
)}
|
className='min-w-0 flex-1 truncate'
|
||||||
|
title={option.label}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
<span className='font-mono text-xs text-muted-foreground'>
|
||||||
|
{option.value}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{description && <FieldDescription>{description}</FieldDescription>}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"country-region-data": "^4.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"echarts": "^6.1.0",
|
"echarts": "^6.1.0",
|
||||||
"echarts-for-react": "^3.0.6",
|
"echarts-for-react": "^3.0.6",
|
||||||
|
|||||||
Generated
+8
@@ -122,6 +122,9 @@ importers:
|
|||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 1.1.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
country-region-data:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -2399,6 +2402,9 @@ packages:
|
|||||||
core-js@3.47.0:
|
core-js@3.47.0:
|
||||||
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
|
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
|
||||||
|
|
||||||
|
country-region-data@4.1.0:
|
||||||
|
resolution: {integrity: sha512-20fGwnpj5Q3HlQTHdMUstpD4pixphymWBj1f9/EonKEvnEDCaq1S3peE1O7NLxHI1zSa6W+5do/u7xS7UVfZtw==}
|
||||||
|
|
||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
@@ -6670,6 +6676,8 @@ snapshots:
|
|||||||
core-js@3.47.0:
|
core-js@3.47.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
country-region-data@4.1.0: {}
|
||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
|
|||||||
Reference in New Issue
Block a user