4 Commits

Author SHA1 Message Date
f5bfff80b7 🚨 Format code 2025-09-24 21:44:42 +08:00
1462d8a753 🐛 Fixed docker prettier plugin issue 2025-09-24 19:14:10 +08:00
39ee2d14f3 Optimize font size and add more fonts 2025-09-24 00:44:41 +08:00
e536cdd9ba 🐛 Fixed docker、shell prettier plugin issue 2025-09-23 19:43:26 +08:00
173 changed files with 5908 additions and 1799 deletions

View File

@@ -50,7 +50,11 @@ export default defineConfig([
'.local',
'/bin',
'Dockerfile',
'**/bindings/'
'**/bindings/',
'*.js',
'**/*.js',
'**/*.cjs',
'**/*.mjs',
],
}
]);

View File

@@ -41,7 +41,6 @@
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@prettier/plugin-xml": "^3.4.2",
"@reteps/dockerfmt": "^0.3.6",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"codemirror": "^6.0.2",
@@ -60,7 +59,6 @@
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.92.1",
"sh-syntax": "^0.5.8",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
@@ -1877,15 +1875,6 @@
"prettier": "^3.0.0"
}
},
"node_modules/@reteps/dockerfmt": {
"version": "0.3.6",
"resolved": "https://registry.npmmirror.com/@reteps/dockerfmt/-/dockerfmt-0.3.6.tgz",
"integrity": "sha512-Tb5wIMvBf/nLejTQ61krK644/CEMB/cpiaIFXqGApfGqO3GwcR3qnI0DbmkFVCl2OyEp8LnLX3EkucoL0+tbFg==",
"license": "MIT",
"engines": {
"node": "^v12.20.0 || ^14.13.0 || >=16.0.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.29",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
@@ -6227,21 +6216,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/sh-syntax": {
"version": "0.5.8",
"resolved": "https://registry.npmmirror.com/sh-syntax/-/sh-syntax-0.5.8.tgz",
"integrity": "sha512-JfVoxf4FxQI5qpsPbkHhZo+n6N9YMJobyl4oGEUBb/31oQYlgTjkXQD8PBiafS2UbWoxrTO0Z5PJUBXEPAG1Zw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/sh-syntax"
}
},
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmmirror.com/sha.js/-/sha.js-2.4.12.tgz",

View File

@@ -45,7 +45,6 @@
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@prettier/plugin-xml": "^3.4.2",
"@reteps/dockerfmt": "^0.3.6",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"codemirror": "^6.0.2",
@@ -64,7 +63,6 @@
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.92.1",
"sh-syntax": "^0.5.8",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
鸿蒙字体压缩工具
使用 fonttools 库压缩 TTF 字体文件,减小文件大小
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
from typing import List, Tuple
def check_dependencies():
"""检查必要的依赖是否已安装"""
missing_packages = []
# 检查 fonttools
try:
import fontTools
except ImportError:
missing_packages.append('fonttools')
# 检查 brotli
try:
import brotli
except ImportError:
missing_packages.append('brotli')
# 检查 pyftsubset 命令是否可用
try:
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
if result.returncode != 0:
missing_packages.append('fonttools[subset]')
except FileNotFoundError:
if 'fonttools' not in missing_packages:
missing_packages.append('fonttools[subset]')
if missing_packages:
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def get_file_size(file_path: str) -> int:
"""获取文件大小(字节)"""
return os.path.getsize(file_path)
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes / (1024 * 1024):.2f} MB"
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
"""
压缩单个字体文件
Args:
input_path: 输入字体文件路径
output_path: 输出字体文件路径
compression_level: 压缩级别 ("basic", "medium", "aggressive")
Returns:
bool: 压缩是否成功
"""
try:
# 基础压缩参数
base_args = [
"pyftsubset", input_path,
"--output-file=" + output_path,
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
]
# 根据压缩级别设置不同的参数
if compression_level == "basic":
# 基础压缩:保留常用字符和功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
"--layout-features=*", # 保留所有布局特性
"--glyph-names", # 保留字形名称
"--symbol-cmap", # 保留符号映射
"--legacy-cmap", # 保留传统字符映射
"--notdef-glyph", # 保留 .notdef 字形
"--recommended-glyphs", # 保留推荐字形
"--name-IDs=*", # 保留所有名称ID
"--name-legacy", # 保留传统名称
]
elif compression_level == "medium":
# 中等压缩:移除一些不常用的功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
"--layout-features=kern,liga,clig", # 只保留关键布局特性
"--no-glyph-names", # 移除字形名称
"--notdef-glyph",
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
]
else: # aggressive
# 激进压缩:最大程度减小文件大小
args = base_args + [
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
"--no-layout-features", # 移除所有布局特性
"--no-glyph-names", # 移除字形名称
"--no-symbol-cmap", # 移除符号映射
"--no-legacy-cmap", # 移除传统映射
"--notdef-glyph",
"--name-IDs=1,2", # 只保留最基本的名称
"--desubroutinize", # 去子程序化可能减小CFF字体大小
]
# 执行压缩命令
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
return True
else:
print(f"压缩失败: {result.stderr}")
return False
except Exception as e:
print(f"压缩过程中出现错误: {str(e)}")
return False
def find_font_files(directory: str) -> List[str]:
"""查找目录中的所有字体文件"""
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
font_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in font_extensions):
font_files.append(os.path.join(root, file))
return font_files
def compress_fonts_batch(font_directory: str, compression_level: str = "basic"):
"""
批量压缩字体文件
Args:
font_directory: 字体文件目录
compression_level: 压缩级别
"""
if not os.path.exists(font_directory):
print(f"错误: 目录 {font_directory} 不存在")
return
# 查找所有字体文件
font_files = find_font_files(font_directory)
if not font_files:
print("未找到字体文件")
return
print(f"找到 {len(font_files)} 个字体文件")
print(f"压缩级别: {compression_level}")
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
print("-" * 60)
total_original_size = 0
total_compressed_size = 0
successful_compressions = 0
for i, font_file in enumerate(font_files, 1):
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
# 获取原始文件大小
original_size = get_file_size(font_file)
total_original_size += original_size
# 生成输出文件名(保持原文件名,只改变扩展名)
file_dir = os.path.dirname(font_file)
base_name = os.path.splitext(os.path.basename(font_file))[0]
output_file = os.path.join(file_dir, f"{base_name}.woff2")
# 压缩字体
if compress_font(font_file, output_file, compression_level):
if os.path.exists(output_file):
compressed_size = get_file_size(output_file)
total_compressed_size += compressed_size
successful_compressions += 1
# 计算压缩率
compression_ratio = (1 - compressed_size / original_size) * 100
print(f" ✓ 成功: {format_file_size(original_size)}{format_file_size(compressed_size)} "
f"(压缩 {compression_ratio:.1f}%)")
else:
print(f" ✗ 失败: 输出文件未生成")
else:
print(f" ✗ 失败: 压缩过程出错")
print()
# 显示总结
print("=" * 60)
print("压缩完成!")
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
if successful_compressions > 0:
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
print(f"总大小: {format_file_size(total_original_size)}{format_file_size(total_compressed_size)}")
print(f"总压缩率: {total_compression_ratio:.1f}%")
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
def main():
"""主函数"""
print("鸿蒙字体压缩工具")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 获取当前脚本所在目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 设置默认字体目录
font_directory = current_dir
print(f"字体目录: {font_directory}")
# 让用户选择压缩级别
print("\n请选择压缩级别:")
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
print("2. 中等压缩 (平衡文件大小和功能)")
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
while True:
choice = input("\n请输入选择 (1-3): ").strip()
if choice == "1":
compression_level = "basic"
break
elif choice == "2":
compression_level = "medium"
break
elif choice == "3":
compression_level = "aggressive"
break
else:
print("无效选择,请输入 1、2 或 3")
# 开始批量压缩
compress_fonts_batch(font_directory, compression_level=compression_level)
if __name__ == "__main__":
main()

View File

@@ -1,146 +0,0 @@
/* HarmonyOS Sans 字体定义 */
/* HarmonyOS Sans Regular */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Light */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Medium */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Semibold */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Semibold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Bold */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Black */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Black.ttf') format('truetype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans Thin */
@font-face {
font-family: 'HarmonyOS Sans';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Thin.ttf') format('truetype');
font-weight: 100;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC 简体中文字体 */
/* HarmonyOS Sans SC Regular */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Light */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Medium */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Semibold */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Semibold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Bold */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Black */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Black.ttf') format('truetype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Sans SC Thin */
@font-face {
font-family: 'HarmonyOS Sans SC';
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Thin.ttf') format('truetype');
font-weight: 100;
font-style: normal;
font-display: swap;
}
/* 字体加载优化 */
.font-loading {
font-family: system-ui, -apple-system, sans-serif;
}
.font-loaded {
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
/* CodeMirror 专用字体类 */
.cm-harmonyos-font {
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif !important;
font-feature-settings: 'liga' 1, 'calt' 1;
font-variant-ligatures: contextual;
text-rendering: optimizeLegibility;
}

View File

@@ -0,0 +1,31 @@
@font-face {
font-family: 'Hack';
src: url('../fonts/Hack/hack-regular.woff2') format('woff2'),
url('../fonts/Hack/hack-regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('../fonts/Hack/hack-bold.woff2') format('woff2'),
url('../fonts/Hack/hack-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('../fonts/Hack/hack-italic.woff2') format('woff2'),
url('../fonts/Hack/hack-italic.woff') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Hack';
src: url('../fonts/Hack/hack-bolditalic.woff2') format('woff2'),
url('../fonts/Hack/hack-bolditalic.woff') format('woff');
font-weight: 700;
font-style: italic;
}

View File

@@ -0,0 +1,134 @@
/* HarmonyOS 字体定义 */
/* HarmonyOS Regular */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Regular.woff2') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Light */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Light.woff2') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Medium */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Medium.woff2') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Semibold */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Semibold.woff2') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Bold */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Bold.woff2') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Black */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Black.woff2') format('truetype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* HarmonyOS Thin */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_Sans/HarmonyOS_Sans_Thin.woff2') format('truetype');
font-weight: 100;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC 简体中文字体 */
/* HarmonyOS SC Regular */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Regular.woff2') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Light */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Light.woff2') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Medium */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Medium.woff2') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Semibold */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Semibold.woff2') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Bold */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Bold.woff2') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Black */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Black.woff2') format('truetype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* HarmonyOS SC Thin */
@font-face {
font-family: 'HarmonyOS';
src: url('../fonts/HarmonyOS/HarmonyOS_SansSC/HarmonyOS_SansSC_Thin.woff2') format('truetype');
font-weight: 100;
font-style: normal;
font-display: swap;
}
/* 字体加载优化 */
.font-loading {
font-family: system-ui, -apple-system, sans-serif;
}

View File

@@ -1,5 +1,7 @@
/* 导入所有CSS文件 */
@import 'normalize.css';
@import 'variables.css';
@import "fonts.css";
@import 'scrollbar.css';
@import "harmony_fonts.css";
@import 'scrollbar.css';
@import 'hack_fonts.css';
@import 'opensans_fonts.css';

View File

@@ -0,0 +1,80 @@
/* BEGIN Light */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/Light/OpenSans-Light.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Light/OpenSans-Light.woff?v=1.1.0") format("woff");
font-weight: 300;
font-style: normal;
}
/* END Light */
/* BEGIN Light Italic */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/LightItalic/OpenSans-LightItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/LightItalic/OpenSans-LightItalic.woff?v=1.1.0") format("woff");
font-weight: 300;
font-style: italic;
}
/* END Light Italic */
/* BEGIN Regular */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/Regular/OpenSans-Regular.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Regular/OpenSans-Regular.woff?v=1.1.0") format("woff");
font-weight: normal;
font-style: normal;
}
/* END Regular */
/* BEGIN Italic */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/Italic/OpenSans-Italic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Italic/OpenSans-Italic.woff?v=1.1.0") format("woff");
font-weight: normal;
font-style: italic;
}
/* END Italic */
/* BEGIN Semibold */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/Semibold/OpenSans-Semibold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Semibold/OpenSans-Semibold.woff?v=1.1.0") format("woff");
font-weight: 600;
font-style: normal;
}
/* END Semibold */
/* BEGIN Semibold Italic */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/SemiboldItalic/OpenSans-SemiboldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/SemiboldItalic/OpenSans-SemiboldItalic.woff?v=1.1.0") format("woff");
font-weight: 600;
font-style: italic;
}
/* END Semibold Italic */
/* BEGIN Bold */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/Bold/OpenSans-Bold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/Bold/OpenSans-Bold.woff?v=1.1.0") format("woff");
font-weight: bold;
font-style: normal;
}
/* END Bold */
/* BEGIN Bold Italic */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/BoldItalic/OpenSans-BoldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/BoldItalic/OpenSans-BoldItalic.woff?v=1.1.0") format("woff");
font-weight: bold;
font-style: italic;
}
/* END Bold Italic */
/* BEGIN Extrabold */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/ExtraBold/OpenSans-ExtraBold.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/ExtraBold/OpenSans-ExtraBold.woff?v=1.1.0") format("woff");
font-weight: 800;
font-style: normal;
}
/* END Extrabold */
/* BEGIN Extrabold Italic */
@font-face {
font-family: 'Open Sans';
src: url("../fonts/OpenSans/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff2?v=1.1.0") format("woff2"), url("../fonts/OpenSans/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff?v=1.1.0") format("woff");
font-weight: 800;
font-style: italic;
}
/* END Extrabold Italic */

View File

@@ -0,0 +1,164 @@
import {
AppConfig,
AppearanceConfig,
EditingConfig,
GeneralConfig,
LanguageType,
SystemThemeType,
TabType,
UpdatesConfig,
UpdateSourceType,
GitBackupConfig,
AuthMethod
} from '@/../bindings/voidraft/internal/models/models';
import {FONT_OPTIONS} from './fonts';
// 配置键映射和限制的类型定义
export type GeneralConfigKeyMap = {
readonly [K in keyof GeneralConfig]: string;
};
export type EditingConfigKeyMap = {
readonly [K in keyof EditingConfig]: string;
};
export type AppearanceConfigKeyMap = {
readonly [K in keyof AppearanceConfig]: string;
};
export type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string;
};
export type BackupConfigKeyMap = {
readonly [K in keyof GitBackupConfig]: string;
};
export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
// 配置键映射
export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
alwaysOnTop: 'general.alwaysOnTop',
dataPath: 'general.dataPath',
enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey',
enableWindowSnap: 'general.enableWindowSnap',
enableLoadingAnimation: 'general.enableLoadingAnimation',
} as const;
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
fontSize: 'editing.fontSize',
fontFamily: 'editing.fontFamily',
fontWeight: 'editing.fontWeight',
lineHeight: 'editing.lineHeight',
enableTabIndent: 'editing.enableTabIndent',
tabSize: 'editing.tabSize',
tabType: 'editing.tabType',
autoSaveDelay: 'editing.autoSaveDelay'
} as const;
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language',
systemTheme: 'appearance.systemTheme'
} as const;
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
version: 'updates.version',
autoUpdate: 'updates.autoUpdate',
primarySource: 'updates.primarySource',
backupSource: 'updates.backupSource',
backupBeforeUpdate: 'updates.backupBeforeUpdate',
updateTimeout: 'updates.updateTimeout',
github: 'updates.github',
gitea: 'updates.gitea'
} as const;
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
enabled: 'backup.enabled',
repo_url: 'backup.repo_url',
auth_method: 'backup.auth_method',
username: 'backup.username',
password: 'backup.password',
token: 'backup.token',
ssh_key_path: 'backup.ssh_key_path',
ssh_key_passphrase: 'backup.ssh_key_passphrase',
backup_interval: 'backup.backup_interval',
auto_backup: 'backup.auto_backup',
} as const;
// 配置限制
export const CONFIG_LIMITS = {
fontSize: {min: 12, max: 28, default: 13},
tabSize: {min: 2, max: 8, default: 4},
lineHeight: {min: 1.0, max: 3.0, default: 1.5},
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
} as const;
// 默认配置
export const DEFAULT_CONFIG: AppConfig = {
general: {
alwaysOnTop: false,
dataPath: '',
enableSystemTray: true,
startAtLogin: false,
enableGlobalHotkey: false,
globalHotkey: {
ctrl: false,
shift: false,
alt: true,
win: false,
key: 'X'
},
enableWindowSnap: true,
enableLoadingAnimation: true,
},
editing: {
fontSize: CONFIG_LIMITS.fontSize.default,
fontFamily: FONT_OPTIONS[0].value,
fontWeight: 'normal',
lineHeight: CONFIG_LIMITS.lineHeight.default,
enableTabIndent: true,
tabSize: CONFIG_LIMITS.tabSize.default,
tabType: CONFIG_LIMITS.tabType.default,
autoSaveDelay: 5000
},
appearance: {
language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto
},
updates: {
version: "1.0.0",
autoUpdate: true,
primarySource: UpdateSourceType.UpdateSourceGithub,
backupSource: UpdateSourceType.UpdateSourceGitea,
backupBeforeUpdate: true,
updateTimeout: 30,
github: {
owner: "landaiqing",
repo: "voidraft",
},
gitea: {
baseURL: "https://git.landaiqing.cn",
owner: "landaiqing",
repo: "voidraft",
}
},
backup: {
enabled: false,
repo_url: "",
auth_method: AuthMethod.UserPass,
username: "",
password: "",
token: "",
ssh_key_path: "",
ssh_key_passphrase: "",
backup_interval: 60,
auto_backup: true,
},
metadata: {
version: '1.0.0',
lastUpdated: new Date().toString(),
}
};

View File

@@ -0,0 +1,101 @@
// Font options with popular programming and common fonts
export const FONT_OPTIONS = [
// Custom fonts
{
label: 'HarmonyOS',
value: 'HarmonyOS'
},
{
label: 'Hack',
value: 'Hack'
},
{
label: 'Open Sans',
value: '"Open Sans"'
},
// Common system fonts
{
label: 'Arial',
value: 'Arial'
},
{
label: 'Helvetica',
value: 'Helvetica'
},
{
label: 'Times New Roman',
value: '"Times New Roman"'
},
{
label: 'Georgia',
value: 'Georgia'
},
{
label: 'Verdana',
value: 'Verdana'
},
{
label: 'Tahoma',
value: 'Tahoma'
},
{
label: 'Segoe UI',
value: '"Segoe UI"'
},
{
label: 'System UI',
value: 'system-ui'
},
// Chinese fonts
{
label: 'Microsoft YaHei',
value: '"Microsoft YaHei"'
},
{
label: 'PingFang SC',
value: '"PingFang SC"'
},
// Popular programming fonts
{
label: 'JetBrains Mono',
value: '"JetBrains Mono"'
},
{
label: 'Fira Code',
value: '"Fira Code"'
},
{
label: 'Source Code Pro',
value: '"Source Code Pro"'
},
{
label: 'Cascadia Code',
value: '"Cascadia Code"'
},
{
label: 'Consolas',
value: 'Consolas'
},
{
label: 'Monaco',
value: 'Monaco'
},
{
label: 'Menlo',
value: 'Menlo'
},
{
label: 'Roboto Mono',
value: '"Roboto Mono"'
},
{
label: 'Inconsolata',
value: 'Inconsolata'
},
{
label: 'Ubuntu Mono',
value: '"Ubuntu Mono"'
}
];

View File

@@ -0,0 +1,13 @@
export type SupportedLocaleType = 'zh-CN' | 'en-US';
// 支持的语言列表
export const SUPPORTED_LOCALES = [
{
code: 'zh-CN' as SupportedLocaleType,
name: '简体中文'
},
{
code: 'en-US' as SupportedLocaleType,
name: 'English'
}
] as const;

View File

@@ -78,7 +78,7 @@ _69: () => {
_70: () => {
return typeof process != "undefined" &&
Object.prototype.toString.call(process) == "[object process]" &&
process.platform == "win32"
process.platform == "win32";
},
_85: s => JSON.stringify(s),
_86: s => printToConsole(s),
@@ -126,7 +126,7 @@ _157: Function.prototype.call.bind(DataView.prototype.setFloat32),
_158: Function.prototype.call.bind(DataView.prototype.getFloat64),
_159: Function.prototype.call.bind(DataView.prototype.setFloat64),
_165: x0 => format = x0,
_166: f => finalizeWrapper(f, function(x0,x1,x2) { return dartInstance.exports._166(f,arguments.length,x0,x1,x2) }),
_166: f => finalizeWrapper(f, function(x0,x1,x2) { return dartInstance.exports._166(f,arguments.length,x0,x1,x2); }),
_184: (c) =>
queueMicrotask(() => dartInstance.exports.$invokeCallback(c)),
_187: (s, m) => {
@@ -337,14 +337,14 @@ _272: (x0,x1) => x0.lastIndex = x1
});
return dartInstance;
}
};
// Call the main function for the instantiated module
// `moduleInstance` is the instantiated dart2wasm module
// `args` are any arguments that should be passed into the main function.
export const invoke = (moduleInstance, ...args) => {
moduleInstance.exports.$invokeMain(args);
}
};
export let format;

View File

@@ -0,0 +1,23 @@
// Format options interface for Dockerfile
export interface FormatOptions {
indent?: number;
trailingNewline?: boolean;
spaceRedirects?: boolean;
}
// Initialize the WASM module
declare function init(wasmUrl?: string): Promise<void>;
// Format Dockerfile content
export declare function format(text: string, options?: FormatOptions): string;
// Format Dockerfile contents (alias for compatibility)
export declare function formatDockerfileContents(
fileContents: string,
options?: FormatOptions
): string;
// Placeholder for Node.js compatibility (not implemented in browser)
export declare function formatDockerfile(): never;
export default init;

View File

@@ -0,0 +1,86 @@
import './wasm_exec.js'
// Format options for Dockerfile
export const FormatOptions = {
indentSize: 4,
trailingNewline: true,
spaceRedirects: false,
}
let wasmInstance = null;
let isInitialized = false;
// Initialize the WASM module
export default async function init(wasmUrl) {
if (isInitialized) {
return;
}
try {
// Load WASM file
const wasmPath = wasmUrl || new URL('./docker_fmt.wasm', import.meta.url).href;
const wasmResponse = await fetch(wasmPath);
const wasmBytes = await wasmResponse.arrayBuffer();
// Initialize Go runtime
const go = new Go();
const result = await WebAssembly.instantiate(wasmBytes, go.importObject);
wasmInstance = result.instance;
// Run the Go program (don't await, as it needs to stay alive)
go.run(wasmInstance);
// Wait a bit for the Go program to initialize and register the function
await new Promise(resolve => setTimeout(resolve, 100));
isInitialized = true;
} catch (error) {
console.error('Failed to initialize Dockerfile WASM module:', error);
throw error;
}
}
// Format Dockerfile content
export function format(text, options = {}) {
if (!isInitialized) {
throw new Error('WASM module not initialized. Call init() first.');
}
if (typeof globalThis.dockerFormat !== 'function') {
throw new Error('dockerFormat function not available. WASM module may not be properly initialized.');
}
const config = {
indentSize: options.indentSize || options.indent || 4,
trailingNewline: options.trailingNewline !== undefined ? options.trailingNewline : true,
spaceRedirects: options.spaceRedirects !== undefined ? options.spaceRedirects : false
};
try {
// Call the dockerFormat function registered by Go
const result = globalThis.dockerFormat(text, config);
// Check if there was an error
if (result && Array.isArray(result) && result[0] === true) {
throw new Error(result[1] || 'Unknown formatting error');
}
// Return the formatted result
return result && Array.isArray(result) ? result[1] : result;
} catch (error) {
console.warn('Dockerfile formatting error:', error);
throw error;
}
}
// Format Dockerfile contents (alias for compatibility)
export function formatDockerfileContents(fileContents, options) {
return format(fileContents, options);
}
// Placeholder for Node.js compatibility (not implemented in browser)
export function formatDockerfile() {
throw new Error(
'`formatDockerfile` is not implemented in the browser. Use `format` or `formatDockerfileContents` instead.',
);
}

View File

@@ -0,0 +1,8 @@
import initAsync from './docker_fmt.js'
import wasm_url from './docker_fmt.wasm?url'
export default function init() {
return initAsync(wasm_url)
}
export * from './docker_fmt.js'

View File

@@ -0,0 +1,55 @@
//go:build js || wasm
// tinygo build -o docker_fmt.wasm -target wasm --no-debug
package main
import (
"strings"
"syscall/js"
"docker_fmt/lib"
)
func Format(this js.Value, args []js.Value) any {
if len(args) < 1 {
return []any{true, "missing input argument"}
}
input := args[0].String()
// Default configuration
indentSize := uint(4)
newlineFlag := true
spaceRedirects := false
// Parse optional configuration if provided
if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() {
config := args[1]
if !config.Get("indentSize").IsUndefined() {
indentSize = uint(config.Get("indentSize").Int())
}
if !config.Get("trailingNewline").IsUndefined() {
newlineFlag = config.Get("trailingNewline").Bool()
}
if !config.Get("spaceRedirects").IsUndefined() {
spaceRedirects = config.Get("spaceRedirects").Bool()
}
}
originalLines := strings.SplitAfter(input, "\n")
c := &lib.Config{
IndentSize: indentSize,
TrailingNewline: newlineFlag,
SpaceRedirects: spaceRedirects,
}
result := lib.FormatFileLines(originalLines, c)
return []any{false, result}
}
func main() {
done := make(chan bool)
js.Global().Set("dockerFormat", js.FuncOf(Format))
<-done
}

View File

@@ -0,0 +1,17 @@
module docker_fmt
go 1.25.0
require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/moby/buildkit v0.24.0
mvdan.cc/sh/v3 v3.12.0
)
require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

View File

@@ -0,0 +1,71 @@
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/moby/buildkit v0.20.2 h1:qIeR47eQ1tzI1rwz0on3Xx2enRw/1CKjFhoONVcTlMA=
github.com/moby/buildkit v0.20.2/go.mod h1:DhaF82FjwOElTftl0JUAJpH/SUIUx4UvcFncLeOtlDI=
github.com/moby/buildkit v0.24.0 h1:qYfTl7W1SIJzWDIDCcPT8FboHIZCYfi++wvySi3eyFE=
github.com/moby/buildkit v0.24.0/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reteps/dockerfmt v0.3.7 h1:GChhICBoy6oiTuoLTLFtGnfyBi2qY9dvHBhrcWrN8Zk=
github.com/reteps/dockerfmt v0.3.7/go.mod h1:5lpbp1KzLWaRhL7qB6IEutHoQK3ZcT2Lb5MWPmFts74=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

View File

@@ -0,0 +1,102 @@
import type { Plugin, SupportLanguage, Parser, Printer, SupportOption } from 'prettier';
import dockerfileInit, { format } from './docker_fmt_vite.js';
// Language configuration for Dockerfile
const languages: SupportLanguage[] = [
{
name: 'dockerfile',
parsers: ['dockerfile'],
extensions: ['.docker', '.Dockerfile'],
filenames: ['Dockerfile', 'dockerfile'],
linguistLanguageId: 99,
vscodeLanguageIds: ['dockerfile'],
},
];
// Parser configuration
const parsers: Record<string, Parser<any>> = {
dockerfile: {
parse: (text: string) => {
// For Dockerfile, we don't need complex parsing, just return the text
// The formatting will be handled by the print function
return { type: 'dockerfile', value: text };
},
astFormat: 'dockerfile',
locStart: () => 0,
locEnd: () => 0,
},
};
// Printer configuration
const printers: Record<string, Printer<any>> = {
dockerfile: {
// @ts-expect-error -- Support async printer like shell plugin
async print(path: any, options: any) {
await ensureInitialized();
const text = path.getValue().value || path.getValue();
try {
const formatted = format(text, {
indent: options.tabWidth || 2,
trailingNewline: true,
spaceRedirects: options.spaceRedirects !== false,
});
return formatted;
} catch (error) {
console.warn('Dockerfile formatting error:', error);
return text;
}
},
},
};
// WASM initialization
let isInitialized = false;
let initPromise: Promise<void> | null = null;
async function ensureInitialized(): Promise<void> {
if (isInitialized) {
return Promise.resolve();
}
if (!initPromise) {
initPromise = (async () => {
try {
await dockerfileInit();
isInitialized = true;
} catch (error) {
console.warn('Failed to initialize Dockerfile WASM module:', error);
initPromise = null;
throw error;
}
})();
}
return initPromise;
}
// Plugin options
const options: Record<string, SupportOption> = {
spaceRedirects: {
type: 'boolean',
category: 'Format',
default: true,
description: 'Add spaces around redirect operators',
},
};
// Plugin definition
const plugin: Plugin = {
languages,
parsers,
printers,
options,
defaultOptions: {
tabWidth: 2,
useTabs: false,
spaceRedirects: true,
},
};
export default plugin;
export { languages, parsers, printers, options };

View File

@@ -0,0 +1,800 @@
package lib
import (
"bytes"
"fmt"
"io"
"log"
"os"
"regexp"
"slices"
"strings"
"github.com/google/shlex"
"github.com/moby/buildkit/frontend/dockerfile/command"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"mvdan.cc/sh/v3/syntax"
)
type ExtendedNode struct {
*parser.Node
Children []*ExtendedNode
Next *ExtendedNode
OriginalMultiline string
}
type ParseState struct {
CurrentLine int
Output string
// Needed to pull in comments
AllOriginalLines []string
Config *Config
}
type Config struct {
IndentSize uint
TrailingNewline bool
SpaceRedirects bool
}
func FormatNode(ast *ExtendedNode, c *Config) (string, bool) {
nodeName := strings.ToLower(ast.Node.Value)
dispatch := map[string]func(*ExtendedNode, *Config) string{
command.Add: formatSpaceSeparated,
command.Arg: formatBasic,
command.Cmd: formatCmd,
command.Copy: formatSpaceSeparated,
command.Entrypoint: formatEntrypoint,
command.Env: formatEnv,
command.Expose: formatSpaceSeparated,
command.From: formatSpaceSeparated,
command.Healthcheck: formatBasic,
command.Label: formatLabel,
command.Maintainer: formatMaintainer,
command.Onbuild: FormatOnBuild,
command.Run: formatRun,
command.Shell: formatCmd,
command.StopSignal: formatBasic,
command.User: formatBasic,
command.Volume: formatBasic,
command.Workdir: formatSpaceSeparated,
}
fmtFunc := dispatch[nodeName]
if fmtFunc == nil {
return "", false
// log.Fatalf("Unknown command: %s %s\n", nodeName, ast.OriginalMultiline)
}
return fmtFunc(ast, c), true
}
func (df *ParseState) processNode(ast *ExtendedNode) {
// We don't want to process nodes that don't have a start or end line.
if ast.Node.StartLine == 0 || ast.Node.EndLine == 0 {
return
}
// check if we are on the correct line,
// otherwise get the comments we are missing
if df.CurrentLine != ast.StartLine {
df.Output += FormatComments(df.AllOriginalLines[df.CurrentLine : ast.StartLine-1])
df.CurrentLine = ast.StartLine
}
// if df.Output != "" {
// // If the previous line isn't a comment or newline, add a newline
// lastTwoChars := df.Output[len(df.Output)-2 : len(df.Output)]
// lastNonTrailingNewline := strings.LastIndex(strings.TrimRight(df.Output, "\n"), "\n")
// if lastTwoChars != "\n\n" && df.Output[lastNonTrailingNewline+1] != '#' {
// df.Output += "\n"
// }
// }
output, ok := FormatNode(ast, df.Config)
if ok {
df.Output += output
df.CurrentLine = ast.EndLine
}
// fmt.Printf("CurrentLine: %d, %d\n", df.CurrentLine, ast.EndLine)
// fmt.Printf("Unknown command: %s %s\n", nodeName, ast.OriginalMultiline)
for _, child := range ast.Children {
df.processNode(child)
}
// fmt.Printf("CurrentLine2: %d, %d\n", df.CurrentLine, ast.EndLine)
if ast.Node.Next != nil {
df.processNode(ast.Next)
}
}
func FormatOnBuild(n *ExtendedNode, c *Config) string {
if len(n.Node.Next.Children) == 1 {
// fmt.Printf("Onbuild: %s\n", n.Node.Next.Children[0].Value)
output, ok := FormatNode(n.Next.Children[0], c)
if ok {
return strings.ToUpper(n.Node.Value) + " " + output
}
}
return n.OriginalMultiline
}
func FormatFileLines(fileLines []string, c *Config) string {
result, err := parser.Parse(strings.NewReader(strings.Join(fileLines, "")))
if err != nil {
log.Printf("%s\n", strings.Join(fileLines, ""))
log.Fatalf("Error parsing file: %v", err)
}
parseState := &ParseState{
CurrentLine: 0,
Output: "",
AllOriginalLines: fileLines,
}
rootNode := BuildExtendedNode(result.AST, fileLines)
parseState.Config = c
parseState.processNode(rootNode)
// After all directives are processed, we need to check if we have any trailing comments to add.
if parseState.CurrentLine < len(parseState.AllOriginalLines) {
// Add the rest of the file
parseState.Output += FormatComments(parseState.AllOriginalLines[parseState.CurrentLine:])
}
parseState.Output = strings.TrimRight(parseState.Output, "\n")
// Ensure the output ends with a newline if requested
if c.TrailingNewline {
parseState.Output += "\n"
}
return parseState.Output
}
func BuildExtendedNode(n *parser.Node, fileLines []string) *ExtendedNode {
// Build an extended node from the parser node
// This is used to add the original multiline string to the node
// and to add the original line numbers
if n == nil {
return nil
}
// Create the extended node with the current parser node
en := &ExtendedNode{
Node: n,
Next: nil,
Children: nil,
OriginalMultiline: "", // Default to empty string
}
// If we have valid start and end lines, construct the multiline representation
if n.StartLine > 0 && n.EndLine > 0 {
// Subtract 1 from StartLine because fileLines is 0-indexed while StartLine is 1-indexed
for i := n.StartLine - 1; i < n.EndLine; i++ {
en.OriginalMultiline += fileLines[i]
}
}
// Process all children recursively
if len(n.Children) > 0 {
childrenNodes := make([]*ExtendedNode, 0, len(n.Children))
for _, child := range n.Children {
extChild := BuildExtendedNode(child, fileLines)
if extChild != nil {
childrenNodes = append(childrenNodes, extChild)
}
}
// Replace the children with the processed ones
en.Children = childrenNodes
}
// Process the next node recursively
if n.Next != nil {
extNext := BuildExtendedNode(n.Next, fileLines)
if extNext != nil {
en.Next = extNext
}
}
return en
}
func formatEnv(n *ExtendedNode, c *Config) string {
// Only the legacy format will have a empty 3rd child
if n.Next.Next.Next.Value == "" {
return strings.ToUpper(n.Node.Value) + " " + n.Next.Node.Value + "=" + n.Next.Next.Node.Value + "\n"
}
// Otherwise, we have a valid env command
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
content := StripWhitespace(regexp.MustCompile(" ").Split(originalTrimmed, 2)[1], true)
// Indent all lines with indentSize spaces
re := regexp.MustCompile("(?m)^ *")
content = strings.Trim(re.ReplaceAllString(content, strings.Repeat(" ", int(c.IndentSize))), " ")
return strings.ToUpper(n.Value) + " " + content
}
func formatShell(content string, hereDoc bool, c *Config) string {
// Semicolons require special handling so we don't break the command
// Improved semicolon support: handle escaped semicolons properly
// Check for unescaped semicolons - if found, try to format them properly
if regexp.MustCompile(`[^\\];`).MatchString(content) {
// Split by unescaped semicolons and format each part separately
parts := regexp.MustCompile(`([^\\]);`).Split(content, -1)
if len(parts) > 1 {
var formattedParts []string
for i, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
// Try to format each part individually
formatted := formatSingleCommand(part, hereDoc, c)
formattedParts = append(formattedParts, formatted)
}
// Add semicolon back except for the last part
if i < len(parts)-1 {
formattedParts[len(formattedParts)-1] += ";"
}
}
return strings.Join(formattedParts, " ")
}
// If splitting didn't work, fall back to original content
return content
}
// Grouped expressions aren't formatted well
// See: https://github.com/mvdan/sh/issues/1148
if strings.Contains(content, "{ \\") {
return content
}
if !hereDoc {
// Here lies some cursed magic. Be careful.
// Replace comments with a subshell evaluation -- they won't be run so we can do this.
content = StripWhitespace(content, true)
lineComment := regexp.MustCompile(`(\n\s*)(#.*)`)
lines := strings.SplitAfter(content, "\n")
for i := range lines {
lineTrim := strings.TrimLeft(lines[i], " \t")
if len(lineTrim) >= 1 && lineTrim[0] == '#' {
lines[i] = strings.ReplaceAll(lines[i], "`", "×")
}
}
content = strings.Join(lines, "")
content = lineComment.ReplaceAllString(content, "$1`$2#`\\")
/*
```
foo \
`#comment#`\
&& bar
```
```
foo && \
`#comment#` \
bar
```
*/
commentContinuation := regexp.MustCompile(`(\\(?:\s*` + "`#.*#`" + `\\){1,}\s*)&&`)
content = commentContinuation.ReplaceAllString(content, "&&$1")
// log.Printf("Content0: %s\n", content)
lines = strings.SplitAfter(content, "\n")
/**
if the next line is not a comment, and we didn't start with a continuation, don't add the `&&`.
*/
inContinuation := false
for i := range lines {
lineTrim := strings.Trim(lines[i], " \t\\\n")
// fmt.Printf("LineTrim: %s\n", lineTrim)
nextLine := ""
isComment := false
nextLineIsComment := false
if i+1 < len(lines) {
nextLine = strings.Trim(lines[i+1], " \t\\\n")
}
if len(nextLine) >= 2 && nextLine[:2] == "`#" {
nextLineIsComment = true
}
if len(lineTrim) >= 2 && lineTrim[:2] == "`#" {
isComment = true
}
// fmt.Printf("isComment: %v, nextLineIsComment: %v, inContinuation: %v\n", isComment, nextLineIsComment, inContinuation)
if isComment && (inContinuation || nextLineIsComment) {
lines[i] = strings.Replace(lines[i], "#`\\", "#`&&\\", 1)
}
if len(lineTrim) >= 2 && !isComment && lineTrim[len(lineTrim)-2:] == "&&" {
inContinuation = true
} else if !isComment {
inContinuation = false
}
}
content = strings.Join(lines, "")
}
// Now that we have a valid bash-style command, we can format it with shfmt
// log.Printf("Content1: %s\n", content)
content = formatBash(content, c)
// log.Printf("Content2: %s\n", content)
if !hereDoc {
reBacktickComment := regexp.MustCompile(`([ \t]*)(?:&& )?` + "`(#.*)#` " + `\\`)
content = reBacktickComment.ReplaceAllString(content, "$1$2")
// Fixup the comment indentation
lines := strings.SplitAfter(content, "\n")
prevIsComment := false
prevCommentSpacing := ""
firstLineIsComment := false
for i := range lines {
lineTrim := strings.TrimLeft(lines[i], " \t")
// fmt.Printf("LineTrim: %s, %v\n", lineTrim, prevIsComment)
if len(lineTrim) >= 1 && lineTrim[0] == '#' {
if i == 0 {
firstLineIsComment = true
lines[i] = strings.Repeat(" ", int(c.IndentSize)) + lineTrim
}
lineParts := strings.SplitN(lines[i], "#", 2)
if prevIsComment {
lines[i] = prevCommentSpacing + "#" + lineParts[1]
} else {
prevCommentSpacing = lineParts[0]
}
prevIsComment = true
} else {
prevIsComment = false
}
}
// TODO: this formatting isn't perfect (see tests/out/run5.dockerfile)
if firstLineIsComment {
lines = slices.Insert(lines, 0, "\\\n")
}
content = strings.Join(lines, "")
content = strings.ReplaceAll(content, "×", "`")
}
return content
}
// formatSingleCommand formats a single shell command (used for semicolon-separated commands)
func formatSingleCommand(content string, hereDoc bool, c *Config) string {
// Grouped expressions aren't formatted well
// See: https://github.com/mvdan/sh/issues/1148
if strings.Contains(content, "{ \\") {
return content
}
if !hereDoc {
// Here lies some cursed magic. Be careful.
// Replace comments with a subshell evaluation -- they won't be run so we can do this.
content = StripWhitespace(content, true)
content = regexp.MustCompile(`#.*`).ReplaceAllString(content, "$(: comment)")
content = strings.ReplaceAll(content, "\\\n", " ")
content = strings.ReplaceAll(content, "\n", " ")
content = regexp.MustCompile(`\s+`).ReplaceAllString(content, " ")
content = strings.TrimSpace(content)
}
return formatBash(content, c)
}
func formatRun(n *ExtendedNode, c *Config) string {
// Get the original RUN command text
hereDoc := false
flags := n.Node.Flags
var content string
if len(n.Node.Heredocs) >= 1 {
content = n.Node.Heredocs[0].Content
hereDoc = true
// Check if heredoc FileDescriptor is 0 (stdin) - this is the standard for RUN commands
if n.Node.Heredocs[0].FileDescriptor != 0 {
log.Printf("Warning: heredoc FileDescriptor is %d, expected 0 for RUN command", n.Node.Heredocs[0].FileDescriptor)
}
} else {
// We split the original multiline string by whitespace
originalText := n.OriginalMultiline
if n.OriginalMultiline == "" {
// If the original multiline string is empty, use the original value
originalText = n.Node.Original
}
originalTrimmed := strings.TrimLeft(originalText, " \t")
parts := regexp.MustCompile("[ \t]").Split(originalTrimmed, 2+len(flags))
content = parts[1+len(flags)]
}
// Try to parse as JSON
jsonItems, err := parseJSONStringArray(content)
if err == nil {
outStr := marshalStringArray(jsonItems)
outStr = strings.ReplaceAll(outStr, "\",\"", "\", \"")
content = outStr + "\n"
} else {
content = formatShell(content, hereDoc, c)
if hereDoc {
n.Node.Heredocs[0].Content = content
content, _ = GetHeredoc(n)
}
}
if len(flags) > 0 {
content = strings.Join(flags, " ") + " " + content
}
return strings.ToUpper(n.Value) + " " + content
}
func GetHeredoc(n *ExtendedNode) (string, bool) {
if len(n.Node.Heredocs) == 0 {
return "", false
}
// printAST(n, 0)
args := []string{}
cur := n.Next
for cur != nil {
if cur.Node.Value != "" {
args = append(args, cur.Node.Value)
}
cur = cur.Next
}
content := strings.Join(args, " ") + "\n" + n.Node.Heredocs[0].Content + n.Node.Heredocs[0].Name + "\n"
return content, true
}
func formatBasic(n *ExtendedNode, c *Config) string {
// Uppercases the command, and indent the following lines
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
value, success := GetHeredoc(n)
if !success {
value = regexp.MustCompile(" ").Split(originalTrimmed, 2)[1]
}
return IndentFollowingLines(strings.ToUpper(n.Value)+" "+value, c.IndentSize)
}
// marshalStringArray manually creates a JSON array string from a slice of strings
// This avoids using encoding/json which causes reflection issues in WASM
func marshalStringArray(items []string) string {
if len(items) == 0 {
return "[]"
}
var result strings.Builder
result.WriteString("[")
for i, item := range items {
if i > 0 {
result.WriteString(", ")
}
result.WriteString("\"")
// Escape quotes and backslashes in the string
escaped := strings.ReplaceAll(item, "\\", "\\\\")
escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
result.WriteString(escaped)
result.WriteString("\"")
}
result.WriteString("]")
return result.String()
}
// parseJSONStringArray manually parses a JSON array string into a slice of strings
// This avoids using encoding/json which causes reflection issues in WASM
func parseJSONStringArray(jsonStr string) ([]string, error) {
jsonStr = strings.TrimSpace(jsonStr)
if !strings.HasPrefix(jsonStr, "[") || !strings.HasSuffix(jsonStr, "]") {
return nil, fmt.Errorf("not a JSON array")
}
// Remove brackets
content := strings.TrimSpace(jsonStr[1 : len(jsonStr)-1])
if content == "" {
return []string{}, nil
}
var result []string
var current strings.Builder
inQuotes := false
escaped := false
for i, char := range content {
if escaped {
switch char {
case '"':
current.WriteRune('"')
case '\\':
current.WriteRune('\\')
case 'n':
current.WriteRune('\n')
case 't':
current.WriteRune('\t')
case 'r':
current.WriteRune('\r')
default:
current.WriteRune('\\')
current.WriteRune(char)
}
escaped = false
continue
}
if char == '\\' {
escaped = true
continue
}
if char == '"' {
inQuotes = !inQuotes
continue
}
if !inQuotes && char == ',' {
result = append(result, current.String())
current.Reset()
// Skip whitespace after comma
for i+1 < len(content) && (content[i+1] == ' ' || content[i+1] == '\t') {
i++
}
continue
}
if inQuotes {
current.WriteRune(char)
}
}
// Add the last item
if current.Len() > 0 || len(result) > 0 {
result = append(result, current.String())
}
return result, nil
}
func getCmd(n *ExtendedNode, shouldSplitNode bool) []string {
cmd := []string{}
for node := n; node != nil; node = node.Next {
// Split value by whitespace
rawValue := strings.Trim(node.Node.Value, " \t")
if len(node.Node.Flags) > 0 {
cmd = append(cmd, node.Node.Flags...)
}
// log.Printf("ShouldSplitNode: %v\n", shouldSplitNode)
if shouldSplitNode {
parts, err := shlex.Split(rawValue)
if err != nil {
log.Fatalf("Error splitting: %s\n", node.Node.Value)
}
cmd = append(cmd, parts...)
} else {
cmd = append(cmd, rawValue)
}
}
// log.Printf("getCmd: %v\n", cmd)
return cmd
}
func shouldRunInShell(node string) bool {
// https://docs.docker.com/reference/dockerfile/#entrypoint
parts, err := shlex.Split(node)
if err != nil {
log.Fatalf("Error splitting: %s\n", node)
}
needsShell := false
// This is a simplistic check to determine if we need to run in a full shell.
for _, part := range parts {
if part == "&&" || part == ";" || part == "||" {
needsShell = true
break
}
}
return needsShell
}
func formatEntrypoint(n *ExtendedNode, c *Config) string {
// this can technically change behavior. https://docs.docker.com/reference/dockerfile/#understand-how-cmd-and-entrypoint-interact
return formatCmd(n, c)
}
func formatCmd(n *ExtendedNode, c *Config) string {
// printAST(n, 0)
isJSON, ok := n.Node.Attributes["json"]
if !ok {
isJSON = false
}
if !isJSON {
doNotSplit := shouldRunInShell(n.Node.Next.Value)
if doNotSplit {
n.Next.Node.Flags = append(n.Next.Node.Flags, []string{"/bin/sh", "-c"}...)
// Hacky workaround to tell getCmd to not split the command
isJSON = true
}
}
cmd := getCmd(n.Next, !isJSON)
bWithSpace := marshalStringArray(cmd)
bWithSpace = strings.ReplaceAll(bWithSpace, "\",\"", "\", \"")
return strings.ToUpper(n.Node.Value) + " " + bWithSpace + "\n"
}
func formatSpaceSeparated(n *ExtendedNode, c *Config) string {
isJSON, ok := n.Node.Attributes["json"]
if !ok {
isJSON = false
}
cmd, success := GetHeredoc(n)
if !success {
cmd = strings.Join(getCmd(n.Next, isJSON), " ")
if len(n.Node.Flags) > 0 {
cmd = strings.Join(n.Node.Flags, " ") + " " + cmd
}
cmd += "\n"
}
return strings.ToUpper(n.Node.Value) + " " + cmd
}
func formatLabel(n *ExtendedNode, c *Config) string {
// Parse LABEL key-value pairs and sort them alphabetically by key
originalTrimmed := strings.TrimLeft(n.OriginalMultiline, " \t")
content := regexp.MustCompile(" ").Split(originalTrimmed, 2)[1]
// Parse key-value pairs
labels := make(map[string]string)
var keys []string
// Split by whitespace and parse key=value pairs
parts := strings.Fields(content)
for _, part := range parts {
if strings.Contains(part, "=") {
kv := strings.SplitN(part, "=", 2)
if len(kv) == 2 {
key := strings.Trim(kv[0], "\"")
value := strings.Trim(kv[1], "\"")
labels[key] = value
keys = append(keys, key)
}
}
}
// If no key-value pairs found, fall back to basic formatting
if len(keys) == 0 {
return formatBasic(n, c)
}
// Sort keys alphabetically
slices.Sort(keys)
// Build sorted output
var result strings.Builder
result.WriteString(strings.ToUpper(n.Value))
result.WriteString(" ")
for i, key := range keys {
if i > 0 {
result.WriteString(" ")
}
result.WriteString(key)
result.WriteString("=\"")
result.WriteString(labels[key])
result.WriteString("\"")
}
result.WriteString("\n")
return result.String()
}
func formatMaintainer(n *ExtendedNode, c *Config) string {
// Get text between quotes
maintainer := strings.Trim(n.Next.Node.Value, "\"")
return "LABEL org.opencontainers.image.authors=\"" + maintainer + "\"\n"
}
func GetFileLines(fileName string) ([]string, error) {
// Open the file
f, err := os.Open(fileName)
if err != nil {
return []string{}, err
}
defer f.Close()
// Read the file contents
b := new(strings.Builder)
io.Copy(b, f)
fileLines := strings.SplitAfter(b.String(), "\n")
return fileLines, nil
}
func StripWhitespace(lines string, rightOnly bool) string {
// Split the string into lines by newlines
// log.Printf("Lines: .%s.\n", lines)
linesArray := strings.SplitAfter(lines, "\n")
// Create a new slice to hold the stripped lines
var strippedLines string
// Iterate over each line
for _, line := range linesArray {
// Trim leading and trailing whitespace
// log.Printf("Line .%s.\n", line)
hadNewline := len(line) > 0 && line[len(line)-1] == '\n'
if rightOnly {
// Only trim trailing whitespace
line = strings.TrimRight(line, " \t\n")
} else {
// Trim both leading and trailing whitespace
line = strings.Trim(line, " \t\n")
}
// log.Printf("Line2 .%s.", line)
if hadNewline {
line += "\n"
}
strippedLines += line
}
return strippedLines
}
func FormatComments(lines []string) string {
// Adds lines to the output, collapsing multiple newlines into a single newline
// and removing leading / trailing whitespace. We can do this because
// we are adding comments and we don't care about the formatting.
missingContent := StripWhitespace(strings.Join(lines, ""), false)
// Replace multiple newlines with a single newline
re := regexp.MustCompile(`\n{3,}`)
return re.ReplaceAllString(missingContent, "\n")
}
func IndentFollowingLines(lines string, indentSize uint) string {
// Split the input by lines
allLines := strings.SplitAfter(lines, "\n")
// If there's only one line or no lines, return the original
if len(allLines) <= 1 {
return lines
}
// Keep the first line as is
result := allLines[0]
// Indent all subsequent lines
for i := 1; i < len(allLines); i++ {
if allLines[i] != "" { // Skip empty lines
// Remove existing indentation and add new indentation
trimmedLine := strings.TrimLeft(allLines[i], " \t")
allLines[i] = strings.Repeat(" ", int(indentSize)) + trimmedLine
}
// Add to result (with newline except for the last line)
result += allLines[i]
}
return result
}
func formatBash(s string, c *Config) string {
r := strings.NewReader(s)
f, err := syntax.NewParser(syntax.KeepComments(true)).Parse(r, "")
if err != nil {
fmt.Printf("Error parsing: %s\n", s)
panic(err)
}
buf := new(bytes.Buffer)
syntax.NewPrinter(
syntax.Minify(false),
syntax.SingleLine(false),
syntax.SpaceRedirects(c.SpaceRedirects),
syntax.Indent(c.IndentSize),
syntax.BinaryNextLine(true),
).Print(buf, f)
return buf.String()
}

View File

@@ -0,0 +1,553 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// This file has been modified for use by the TinyGo compiler.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
global.fs = require("node:fs");
}
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!global.fs) {
let outputBuf = "";
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!global.crypto) {
const nodeCrypto = require("node:crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
if (!global.TextEncoder) {
global.TextEncoder = require("node:util").TextEncoder;
}
if (!global.TextDecoder) {
global.TextDecoder = require("node:util").TextDecoder;
}
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
global.Go = class {
constructor() {
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.memory.buffer);
}
const unboxValue = (v_ref) => {
reinterpretBuf.setBigInt64(0, v_ref, true);
const f = reinterpretBuf.getFloat64(0, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = v_ref & 0xffffffffn;
return this._values[id];
}
const loadValue = (addr) => {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) {
return nanHead << 32n;
}
if (v === 0) {
return (nanHead << 32n) | 1n;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
}
switch (v) {
case undefined:
return 0n;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = BigInt(this._values.length);
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 1n;
switch (typeof v) {
case "string":
typeFlag = 2n;
break;
case "symbol":
typeFlag = 3n;
break;
case "function":
typeFlag = 4n;
break;
}
return id | ((nanHead | typeFlag) << 32n);
}
const storeValue = (addr, v) => {
let v_ref = boxValue(v);
mem().setBigUint64(addr, v_ref, true);
}
const loadSlice = (array, len, cap) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
}
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (ptr, len) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
wasi_snapshot_preview1: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
},
gojs: {
// func ticks() int64
"runtime.ticks": () => {
return BigInt((timeOrigin + performance.now()) * 1e6);
},
// func sleepTicks(timeout int64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(() => {
if (this.exited) return;
try {
this._inst.exports.go_scheduler();
} catch (e) {
if (e !== wasmExit) throw e;
}
}, Number(timeout)/1e6);
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (v_ref) => {
// Note: TinyGo does not support finalizers so this is only called
// for one specific case, by js.go:jsString. and can/might leak memory.
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (value_ptr, value_len) => {
value_ptr >>>= 0;
const s = loadString(value_ptr, value_len);
return boxValue(s);
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
let prop = loadString(p_ptr, p_len);
let v = unboxValue(v_ref);
let result = Reflect.get(v, prop);
return boxValue(result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (v_ref, i) => {
return boxValue(Reflect.get(unboxValue(v_ref), i));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const m = Reflect.get(v, name);
storeValue(ret_addr, Reflect.apply(m, v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
try {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
storeValue(ret_addr, Reflect.apply(v, undefined, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
storeValue(ret_addr, Reflect.construct(v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr+ 8, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (v_ref) => {
return unboxValue(v_ref).length;
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
const s = String(unboxValue(v_ref));
const str = encoder.encode(s);
storeValue(ret_addr, str);
mem().setInt32(ret_addr + 8, str.length, true);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
const str = unboxValue(v_ref);
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
return unboxValue(v_ref) instanceof unboxValue(t_ref);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
// copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified:
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = unboxValue(dst_ref);
const src = loadSlice(src_addr, src_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
}
};
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
// For compatibility, we use both as long as Go 1.20 is supported.
this.importObject.env = this.importObject.gojs;
}
async run(instance) {
this._inst = instance;
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
global,
this,
];
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this.exitCode = 0;
if (this._inst.exports._start) {
let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});
// Run program, but catch the wasmExit exception that's thrown
// to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}
await exitPromise;
return this.exitCode;
} else {
this._inst.exports._initialize();
}
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
try {
this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
if (
global.require &&
// global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
let exitCode = await go.run(result.instance);
process.exit(exitCode);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})();

View File

@@ -60,7 +60,7 @@ function initGofmt(): Promise<void> {
// Printer configuration
const goPrinter: Printer<string> = {
// @ts-expect-error -- Support async printer like shell plugin
async print(path, options) {
async print(path, _options) {
try {
// Wait for initialization to complete
await initGofmt();

View File

@@ -39,7 +39,7 @@ const groovyPrinter: Printer<string> = {
return groovyBeautify(path.node, {
width: options.printWidth || 80,
}).trim();
} catch (error) {
} catch (_error) {
return path.node;
}
},

View File

@@ -58,7 +58,7 @@ type ModifierNode = JavaNonTerminal & {
annotation?: AnnotationCstNode[];
};
};
type IsTuple<T> = T extends [] ? true : T extends [infer First, ...infer Remain] ? IsTuple<Remain> : false;
type IsTuple<T> = T extends [] ? true : T extends [infer _First, ...infer Remain] ? IsTuple<Remain> : false;
type IndexProperties<T extends {
length: number;
}> = IsTuple<T> extends true ? Exclude<Partial<T>["length"], T["length"]> : number;

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export function format(input: string, filename: string, config?: Config): string;
interface LayoutConfig {

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const __wbindgen_export_0: (a: number, b: number) => number;

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export function format(input: string, path?: string, config?: Config): string;
export interface Config {

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const __wbindgen_export_0: (a: number, b: number) => number;

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export function format(input: string, config?: Config | null): string;
export interface Config {

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const format: (a: number, b: number, c: number, d: number) => void;
export const __wbindgen_export_0: (a: number, b: number) => number;

View File

@@ -1,471 +1,207 @@
import type { Parser, ParserOptions, Plugin, Printer } from 'prettier'
import {
type File,
LangVariant,
type Node,
type ParseError,
type ShOptions,
type ShPrintOptions,
getProcessor,
} from 'sh-syntax'
/**
* Prettier Plugin for Shell formatting using shfmt WebAssembly
*
* This plugin provides support for formatting Shell files using the shfmt WASM implementation.
*/
import type { Plugin, Parser, Printer } from 'prettier';
import { languages } from './languages'
// Import the shell WASM module
import shfmtInit, { format, type Config } from './shfmt_vite.js';
// 创建处理器实例
let processorInstance: any = null
const parserName = 'sh';
const getProcessorInstance = async () => {
if (!processorInstance) {
try {
// @ts-ignore
const initWasm = await import('sh-syntax/main.wasm?init')
processorInstance = getProcessor(initWasm.default)
} catch {
processorInstance = getProcessor(() =>
fetch(new URL('sh-syntax/main.wasm', import.meta.url))
)
}
// Language configuration
const languages = [
{
name: 'Shell',
aliases: ['sh', 'bash', 'shell'],
parsers: [parserName],
extensions: ['.sh', '.bash', '.zsh', '.fish', '.ksh'],
aceMode: 'sh',
tmScope: 'source.shell',
linguistLanguageId: 302,
vscodeLanguageIds: ['shellscript']
}
return processorInstance
}
];
export interface DockerfilePrintOptions extends ParserOptions<string> {
indent?: number
spaceRedirects?: boolean
}
export interface ShParserOptions
extends Partial<ParserOptions<Node>>,
ShOptions {
filepath?: string
}
export type { ShPrintOptions }
export interface ShPrinterOptions extends ShPrintOptions {
filepath?: string
tabWidth: number
}
export class ShSyntaxParseError<
E extends Error = ParseError | SyntaxError,
> extends SyntaxError {
declare cause: E
declare loc: { start: { column: number; line: number } } | undefined
constructor(err: E) {
const error = err as ParseError | SyntaxError
super(('Text' in error && error.Text) || error.message)
this.cause = err
// `error instanceof ParseError` won't not work because the error is thrown wrapped by `synckit`
if ('Pos' in error && error.Pos != null && typeof error.Pos === 'object') {
this.loc = { start: { column: error.Pos.Col, line: error.Pos.Line } }
}
}
}
function hasPragma(text: string) {
/**
* We don't want to parse every file twice but Prettier's interface isn't
* conducive to caching/memoizing an upstream Parser, so we're going with some
* minor Regex hackery.
*
* Only read empty lines, comments, and shebangs at the start of the file. We
* do not support Bash's pseudo-block comments.
*/
// No, we don't support unofficial block comments.
const commentLineRegex = /^\s*(#(?<comment>.*))?$/gm
let lastIndex = -1
/**
* Only read leading comments, skip shebangs, and check for the pragma. We
* don't want to have to parse every file twice.
*/
for (;;) {
const match = commentLineRegex.exec(text)
// Found "real" content, EoF, or stuck in a loop.
if (match == null || match.index !== lastIndex + 1) {
return false
}
lastIndex = commentLineRegex.lastIndex
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- incorrect typing
const comment = match.groups?.comment?.trim()
// Empty lines and shebangs have no captures
if (comment == null) {
continue
}
if (comment.startsWith('@prettier') || comment.startsWith('@format')) {
return true
}
}
}
const dockerfileParser: Parser<string> = {
astFormat: 'dockerfile',
hasPragma,
parse: text => text,
// Parser configuration
const shParser: Parser<string> = {
astFormat: parserName,
parse: (text: string) => text,
locStart: () => 0,
locEnd: node => node.length,
}
locEnd: (node: string) => node.length,
};
let formatDockerfileContents_:
| any
| undefined
// Lazy initialize shfmt WASM module
let initPromise: Promise<void> | null = null;
let isInitialized = false;
const getFormatDockerfileContents = async () => {
if (!formatDockerfileContents_) {
try {
// @ts-ignore - 忽略模块解析错误
const dockerfmt = await import('@reteps/dockerfmt')
formatDockerfileContents_ = dockerfmt.formatDockerfileContents
} catch (error) {
console.warn('Failed to load @reteps/dockerfmt:', error)
formatDockerfileContents_ = null
}
function initShfmt(): Promise<void> {
if (isInitialized) {
return Promise.resolve();
}
return formatDockerfileContents_
if (!initPromise) {
initPromise = (async () => {
try {
await shfmtInit();
isInitialized = true;
} catch (error) {
console.warn('Failed to initialize shfmt WASM module:', error);
initPromise = null;
throw error;
}
})();
}
return initPromise;
}
const dockerPrinter: Printer<string> = {
// @ts-expect-error -- https://github.com/prettier/prettier/issues/15080#issuecomment-1630987744
async print(
path,
{
filepath,
// parser options
keepComments = true,
variant,
stopAt,
recoverErrors,
// printer options
useTabs,
tabWidth,
indent = useTabs ? 0 : (tabWidth ?? 2),
binaryNextLine = true,
switchCaseIndent = true,
spaceRedirects,
// eslint-disable-next-line sonarjs/deprecation
keepPadding,
minify,
singleLine,
functionNextLine,
}: ShPrintOptions,
) {
const formatDockerfileContents = await getFormatDockerfileContents()
// Printer configuration
const shPrinter: Printer<string> = {
// @ts-expect-error -- Support async printer like shell plugin
async print(path, options) {
try {
if (formatDockerfileContents) {
return await formatDockerfileContents(path.node, {
indent,
spaceRedirects: spaceRedirects ?? false,
trailingNewline: true,
})
}
throw new Error('dockerfmt not available')
} catch {
/*
* `dockerfmt` is buggy now and could throw unexpectedly, so we fallback to
* the `sh` printer automatically in this case.
*
* @see {https://github.com/reteps/dockerfmt/issues/21}
* @see {https://github.com/reteps/dockerfmt/issues/25}
*/
const processor = await getProcessorInstance()
return processor(path.node, {
print: true,
filepath,
keepComments,
variant,
stopAt,
recoverErrors,
useTabs,
tabWidth,
indent,
binaryNextLine,
switchCaseIndent,
spaceRedirects: spaceRedirects ?? true,
keepPadding,
minify,
singleLine,
functionNextLine,
})
// Wait for initialization to complete
await initShfmt();
const text = (path as any).getValue ? (path as any).getValue() : path.node;
const config = getShfmtConfig(options);
// Format using shfmt (synchronous call)
const formatted = format(text, config);
return formatted.trim();
} catch (error) {
console.warn('Shell formatting failed:', error);
// Return original text if formatting fails
return (path as any).getValue ? (path as any).getValue() : path.node;
}
},
};
// Helper function to create shfmt config from Prettier options
function getShfmtConfig(options: any): Config {
const config: Config = {};
// Map Prettier options to shfmt config
if (options.useTabs !== undefined) {
config.useTabs = options.useTabs;
}
if (options.tabWidth !== undefined) {
config.tabWidth = options.tabWidth;
}
if (options.printWidth !== undefined) {
config.printWidth = options.printWidth;
}
// Shell-specific options
if (options.shVariant !== undefined) {
config.variant = options.shVariant;
}
if (options.shKeepComments !== undefined) {
config.keepComments = options.shKeepComments;
}
if (options.shBinaryNextLine !== undefined) {
config.binaryNextLine = options.shBinaryNextLine;
}
if (options.shSwitchCaseIndent !== undefined) {
config.switchCaseIndent = options.shSwitchCaseIndent;
}
if (options.shSpaceRedirects !== undefined) {
config.spaceRedirects = options.shSpaceRedirects;
}
if (options.shKeepPadding !== undefined) {
config.keepPadding = options.shKeepPadding;
}
if (options.shFunctionNextLine !== undefined) {
config.functionNextLine = options.shFunctionNextLine;
}
return config;
}
const shParser: Parser<Node> = {
astFormat: 'sh',
hasPragma,
locStart: node => node.Pos.Offset,
locEnd: node => node.End.Offset,
async parse(
text,
{
filepath = '',
keepComments = true,
/**
* The following `@link` doesn't work as expected, see
* {@link https://github.com/microsoft/tsdoc/issues/9}
*/
/** TODO: support {@link LangVariant.LangAuto} */ // eslint-disable-line sonarjs/todo-tag
variant,
stopAt,
recoverErrors,
}: ShParserOptions,
) {
const processor = await getProcessorInstance()
return processor(text, {
filepath,
keepComments,
variant,
stopAt,
recoverErrors,
})
},
}
const shPrinter: Printer<Node | string> = {
// @ts-expect-error -- https://github.com/prettier/prettier/issues/15080#issuecomment-1630987744
async print(
path,
{
originalText,
filepath,
// parser options
keepComments = true,
variant,
stopAt,
recoverErrors,
// printer options
useTabs,
tabWidth,
indent = useTabs ? 0 : tabWidth,
binaryNextLine = true,
switchCaseIndent = true,
spaceRedirects = true,
// eslint-disable-next-line sonarjs/deprecation
keepPadding,
minify,
singleLine,
functionNextLine,
}: ShPrintOptions,
) {
const processor = await getProcessorInstance()
return processor(path.node as File, {
originalText,
filepath,
keepComments,
variant,
stopAt,
recoverErrors,
useTabs,
tabWidth,
indent,
binaryNextLine,
switchCaseIndent,
spaceRedirects,
keepPadding,
minify,
singleLine,
functionNextLine,
})
},
}
export const parsers = {
dockerfile: dockerfileParser,
sh: shParser,
}
export const printers = {
dockerfile: dockerPrinter,
sh: shPrinter,
}
export const options: Plugin['options'] = {
keepComments: {
// since: '0.1.0',
category: 'Output',
type: 'boolean',
default: true,
description:
'KeepComments makes the parser parse comments and attach them to nodes, as opposed to discarding them.',
},
variant: {
// since: '0.1.0',
category: 'Config',
type: 'choice',
// Plugin options
const options = {
shVariant: {
since: '0.0.1',
category: 'Format' as const,
type: 'choice' as const,
default: 'bash',
description: 'Shell variant to use for formatting',
choices: [
{
value: LangVariant.LangBash,
description: [
'LangBash corresponds to the GNU Bash language, as described in its manual at https://www.gnu.org/software/bash/manual/bash.html.',
'',
'We currently follow Bash version 5.2.',
'',
'Its string representation is "bash".',
].join('\n'),
},
{
value: LangVariant.LangPOSIX,
description: [
'LangPOSIX corresponds to the POSIX Shell language, as described at https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.',
'',
'Its string representation is "posix" or "sh".',
].join('\n'),
},
{
value: LangVariant.LangMirBSDKorn,
description: [
'LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.',
'Note that it shares some features with Bash, due to the shared ancestry that is ksh.',
'',
'We currently follow mksh version 59.',
'',
'Its string representation is "mksh".',
].join('\n'),
},
{
value: LangVariant.LangBats,
description: [
'LangBats corresponds to the Bash Automated Testing System language, as described at https://github.com/bats-core/bats-core.',
"Note that it's just a small extension of the Bash language.",
'',
'Its string representation is "bats".',
].join('\n'),
},
{
value: LangVariant.LangAuto,
description: [
"LangAuto corresponds to automatic language detection, commonly used by end-user applications like shfmt, which can guess a file's language variant given its filename or shebang.",
'',
'At this time, [Variant] does not support LangAuto.',
].join('\n'),
},
],
description:
'Variant changes the shell language variant that the parser will accept.',
{ value: 'bash', description: 'Bash shell' },
{ value: 'posix', description: 'POSIX shell' },
{ value: 'mksh', description: 'MirBSD Korn shell' },
{ value: 'bats', description: 'Bats testing framework' }
]
},
stopAt: {
// since: '0.1.0',
category: 'Config',
type: 'path',
description: [
'StopAt configures the lexer to stop at an arbitrary word, treating it as if it were the end of the input. It can contain any characters except whitespace, and cannot be over four bytes in size.',
'This can be useful to embed shell code within another language, as one can use a special word to mark the delimiters between the two.',
'As a word, it will only apply when following whitespace or a separating token. For example, StopAt("$$") will act on the inputs "foo $$" and "foo;$$", but not on "foo \'$$\'".',
'The match is done by prefix, so the example above will also act on "foo $$bar".',
].join('\n'),
},
recoverErrors: {
// since: '0.17.0',
category: 'Config',
type: 'path',
description: [
'RecoverErrors allows the parser to skip up to a maximum number of errors in the given input on a best-effort basis.',
'This can be useful to tab-complete an interactive shell prompt, or when providing diagnostics on slightly incomplete shell source.',
'',
'Currently, this only helps with mandatory tokens from the shell grammar which are not present in the input. They result in position fields or nodes whose position report [Pos.IsRecovered] as true.',
'',
'For example, given the input `(foo |`, the result will contain two recovered positions; first, the pipe requires a statement to follow, and as [Stmt.Pos] reports, the entire node is recovered.',
'Second, the subshell needs to be closed, so [Subshell.Rparen] is recovered.',
].join('\n'),
},
indent: {
// since: '0.1.0',
category: 'Format',
type: 'int',
description:
'Indent sets the number of spaces used for indentation. If set to 0, tabs will be used instead.',
},
binaryNextLine: {
// since: '0.1.0',
category: 'Output',
type: 'boolean',
shKeepComments: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: true,
description:
'BinaryNextLine will make binary operators appear on the next line when a binary command, such as a pipe, spans multiple lines. A backslash will be used.',
description: 'Keep comments in formatted output'
},
switchCaseIndent: {
// since: '0.1.0',
category: 'Format',
type: 'boolean',
shBinaryNextLine: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: true,
description:
'SwitchCaseIndent will make switch cases be indented. As such, switch case bodies will be two levels deeper than the switch itself.',
description: 'Place binary operators on next line'
},
spaceRedirects: {
// since: '0.1.0',
category: 'Format',
type: 'boolean',
shSwitchCaseIndent: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: true,
description:
"SpaceRedirects will put a space after most redirection operators. The exceptions are '>&', '<&', '>(', and '<('.",
description: 'Indent switch case statements'
},
keepPadding: {
// since: '0.1.0',
category: 'Format',
type: 'boolean',
shSpaceRedirects: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: true,
description: 'Add spaces around redirects'
},
shKeepPadding: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: false,
description: [
'KeepPadding will keep most nodes and tokens in the same column that they were in the original source.',
'This allows the user to decide how to align and pad their code with spaces.',
'',
'Note that this feature is best-effort and will only keep the alignment stable, so it may need some human help the first time it is run.',
].join('\n'),
deprecated: [
'This formatting option is flawed and buggy, and often does not result in what the user wants when the code gets complex enough.',
'The next major version, v4, will remove this feature entirely.',
'See: https://github.com/mvdan/sh/issues/658',
].join('\n'),
description: 'Keep padding in column alignment'
},
minify: {
// since: '0.1.0',
category: 'Output',
type: 'boolean',
shFunctionNextLine: {
since: '0.0.1',
category: 'Format' as const,
type: 'boolean' as const,
default: false,
description: [
'Minify will print programs in a way to save the most bytes possible.',
'For example, indentation and comments are skipped, and extra whitespace is avoided when possible.',
].join('\n'),
},
singleLine: {
// since: '0.17.0',
category: 'Format',
type: 'boolean',
default: false,
description: [
'SingleLine will attempt to print programs in one line. For example, lists of commands or nested blocks do not use newlines in this mode.',
'Note that some newlines must still appear, such as those following comments or around here-documents.',
'',
"Print's trailing newline when given a [*File] is not affected by this option.",
].join('\n'),
},
functionNextLine: {
// since: '0.1.0',
category: 'Format',
type: 'boolean',
default: false,
description:
"FunctionNextLine will place a function's opening braces on the next line.",
},
}
description: 'Place function opening brace on next line'
}
};
const shellPlugin: Plugin = {
// Plugin definition
const shPlugin: Plugin = {
languages,
parsers,
printers,
parsers: {
[parserName]: shParser,
},
printers: {
[parserName]: shPrinter,
},
options,
}
};
export default shellPlugin
export { languages }
// Export plugin without auto-initialization
export default shPlugin;
export { languages, initShfmt as initialize };
export const parsers = shPlugin.parsers;
export const printers = shPlugin.printers;

View File

@@ -1,60 +0,0 @@
export const languages = [
{
name: "Shell",
parsers: ["sh"],
extensions: [
".sh",
".bash",
".zsh",
".fish",
".ksh",
".csh",
".tcsh",
".ash",
".dash"
],
filenames: [
"*.sh",
"*.bash",
".bashrc",
".bash_profile",
".bash_login",
".bash_logout",
".zshrc",
".profile"
],
interpreters: [
"bash",
"sh",
"zsh",
"fish",
"ksh",
"csh",
"tcsh",
"ash",
"dash"
],
tmScope: "source.shell",
aceMode: "sh",
codemirrorMode: "shell",
linguistLanguageId: 302,
vscodeLanguageIds: ["shellscript"]
},
{
name: "Dockerfile",
parsers: ["dockerfile"],
extensions: [".dockerfile"],
filenames: [
"Dockerfile",
"*.dockerfile",
"Containerfile",
"*.containerfile"
],
tmScope: "source.dockerfile",
aceMode: "dockerfile",
codemirrorMode: "dockerfile",
linguistLanguageId: 99,
vscodeLanguageIds: ["dockerfile"]
}
];

View File

@@ -0,0 +1,51 @@
/**
* TypeScript definitions for Shell formatter WASM module
*/
// Language variants enum
export declare const LangVariant: {
readonly LangBash: 0;
readonly LangPOSIX: 1;
readonly LangMirBSDKorn: 2;
readonly LangBats: 3;
readonly LangAuto: 4;
};
// Configuration interface
export interface Config {
useTabs?: boolean;
tabWidth?: number;
printWidth?: number;
variant?: number;
keepComments?: boolean;
binaryNextLine?: boolean;
switchCaseIndent?: boolean;
spaceRedirects?: boolean;
keepPadding?: boolean;
functionNextLine?: boolean;
}
// Parse error class
export declare class ParseError extends Error {
Filename?: string;
Incomplete?: boolean;
Text: string;
Pos?: any;
constructor(params: {
Filename?: string;
Incomplete?: boolean;
Text: string;
Pos?: any;
});
}
// Initialize the WASM module
declare function init(wasmUrl?: string): Promise<void>;
export default init;
// Format shell code
export declare function format(text: string, config?: Config): string;
// Parse shell code (returns AST)
export declare function parse(text: string, config?: Config): any;

View File

@@ -0,0 +1,245 @@
/**
* Shell formatter WASM module wrapper
* Based on the existing src implementation but adapted for browser use
*/
// Import WASM execution environment
import './wasm_exec.cjs';
// Language variants enum
export const LangVariant = {
LangBash: 0,
LangPOSIX: 1,
LangMirBSDKorn: 2,
LangBats: 3,
LangAuto: 4,
};
// Parse error class
export class ParseError extends Error {
constructor({ Filename, Incomplete, Text, Pos }) {
super(Text);
this.Filename = Filename;
this.Incomplete = Incomplete;
this.Text = Text;
this.Pos = Pos;
}
}
let encoder;
let decoder;
let wasmInstance = null;
let isInitialized = false;
// Initialize the WASM module
export default async function init(wasmUrl) {
if (isInitialized) {
return;
}
encoder = new TextEncoder();
decoder = new TextDecoder();
try {
// Load WASM file
const wasmPath = wasmUrl || new URL('./shfmt.wasm', import.meta.url).href;
const wasmResponse = await fetch(wasmPath);
const wasmBytes = await wasmResponse.arrayBuffer();
// Initialize Go runtime
const go = new ShellGo();
const result = await WebAssembly.instantiate(wasmBytes, go.importObject);
wasmInstance = result.instance;
// Run the Go program
go.run(wasmInstance);
isInitialized = true;
} catch (error) {
console.error('Failed to initialize shfmt WASM module:', error);
throw error;
}
}
// Format shell code
export function format(text, config = {}) {
if (!isInitialized || !wasmInstance) {
throw new Error('WASM module not initialized. Call init() first.');
}
const {
variant = LangVariant.LangBash,
keepComments = true,
useTabs = false,
tabWidth = 2,
binaryNextLine = true,
switchCaseIndent = true,
spaceRedirects = true,
keepPadding = false,
functionNextLine = false
} = config;
const indent = useTabs ? 0 : tabWidth;
try {
const { memory, wasmAlloc, wasmFree, process } = wasmInstance.exports;
// Encode input text
const encodedText = encoder.encode(text);
const encodedFilePath = encoder.encode('input.sh');
const encodedStopAt = encoder.encode('');
// Allocate memory for inputs
const filePathPointer = wasmAlloc(encodedFilePath.byteLength);
new Uint8Array(memory.buffer).set(encodedFilePath, filePathPointer);
const textPointer = wasmAlloc(encodedText.byteLength);
new Uint8Array(memory.buffer).set(encodedText, textPointer);
const stopAtPointer = wasmAlloc(encodedStopAt.byteLength);
new Uint8Array(memory.buffer).set(encodedStopAt, stopAtPointer);
// Call the process function
const resultPointer = process(
filePathPointer, encodedFilePath.byteLength, encodedFilePath.byteLength,
textPointer, encodedText.byteLength, encodedText.byteLength,
true, // print mode
keepComments,
variant,
stopAtPointer, encodedStopAt.byteLength, encodedStopAt.byteLength,
0, // recoverErrors
indent,
binaryNextLine,
switchCaseIndent,
spaceRedirects,
keepPadding,
false, // minify
false, // singleLine
functionNextLine
);
// Free allocated memory
wasmFree(filePathPointer);
wasmFree(textPointer);
wasmFree(stopAtPointer);
// Read result
const result = new Uint8Array(memory.buffer).subarray(resultPointer);
const end = result.indexOf(0);
const resultString = decoder.decode(result.subarray(0, end));
// Parse result
if (!resultString.startsWith('{"') || !resultString.endsWith('}')) {
throw new ParseError({
Filename: 'input.sh',
Incomplete: true,
Text: resultString,
});
}
const { file, text: processedText, parseError, message } = JSON.parse(resultString);
if (parseError || message) {
throw parseError == null
? new SyntaxError(message)
: new ParseError(parseError);
}
return processedText || text;
} catch (error) {
console.warn('Shell formatting error:', error);
throw error;
}
}
// Parse shell code (returns AST)
export function parse(text, config = {}) {
if (!isInitialized || !wasmInstance) {
throw new Error('WASM module not initialized. Call init() first.');
}
const {
variant = LangVariant.LangBash,
keepComments = true,
useTabs = false,
tabWidth = 2,
binaryNextLine = true,
switchCaseIndent = true,
spaceRedirects = true,
keepPadding = false,
functionNextLine = false
} = config;
const indent = useTabs ? 0 : tabWidth;
try {
const { memory, wasmAlloc, wasmFree, process } = wasmInstance.exports;
// Encode input text
const encodedText = encoder.encode(text);
const encodedFilePath = encoder.encode('input.sh');
const encodedStopAt = encoder.encode('');
// Allocate memory for inputs
const filePathPointer = wasmAlloc(encodedFilePath.byteLength);
new Uint8Array(memory.buffer).set(encodedFilePath, filePathPointer);
const textPointer = wasmAlloc(encodedText.byteLength);
new Uint8Array(memory.buffer).set(encodedText, textPointer);
const stopAtPointer = wasmAlloc(encodedStopAt.byteLength);
new Uint8Array(memory.buffer).set(encodedStopAt, stopAtPointer);
// Call the process function
const resultPointer = process(
filePathPointer, encodedFilePath.byteLength, encodedFilePath.byteLength,
textPointer, encodedText.byteLength, encodedText.byteLength,
false, // parse mode
keepComments,
variant,
stopAtPointer, encodedStopAt.byteLength, encodedStopAt.byteLength,
0, // recoverErrors
indent,
binaryNextLine,
switchCaseIndent,
spaceRedirects,
keepPadding,
false, // minify
false, // singleLine
functionNextLine
);
// Free allocated memory
wasmFree(filePathPointer);
wasmFree(textPointer);
wasmFree(stopAtPointer);
// Read result
const result = new Uint8Array(memory.buffer).subarray(resultPointer);
const end = result.indexOf(0);
const resultString = decoder.decode(result.subarray(0, end));
// Parse result
if (!resultString.startsWith('{"') || !resultString.endsWith('}')) {
throw new ParseError({
Filename: 'input.sh',
Incomplete: true,
Text: resultString,
});
}
const { file, text: processedText, parseError, message } = JSON.parse(resultString);
if (parseError || message) {
throw parseError == null
? new SyntaxError(message)
: new ParseError(parseError);
}
return file;
} catch (error) {
console.warn('Shell parsing error:', error);
throw error;
}
}

Binary file not shown.

View File

@@ -0,0 +1,8 @@
import initAsync from './shfmt.js'
import wasm_url from './shfmt.wasm?url'
export default function init() {
return initAsync(wasm_url)
}
export * from './shfmt.js'

View File

@@ -0,0 +1,10 @@
module shell_fmt
go 1.25.0
require (
github.com/mailru/easyjson v0.9.1
mvdan.cc/sh/v3 v3.12.0
)
require github.com/josharian/intern v1.0.0 // indirect

View File

@@ -0,0 +1,16 @@
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

View File

@@ -0,0 +1,95 @@
// tinygo build -o shfmt.wasm -target=wasm --no-debug
package src
import (
"bytes"
"io"
"mvdan.cc/sh/v3/syntax"
)
var (
parser *syntax.Parser
printer *syntax.Printer
)
type ParserOptions struct {
KeepComments bool
Variant syntax.LangVariant
StopAt string
RecoverErrors int
}
type PrinterOptions struct {
Indent uint
BinaryNextLine bool
SwitchCaseIndent bool
SpaceRedirects bool
KeepPadding bool
Minify bool
SingleLine bool
FunctionNextLine bool
}
type SyntaxOptions struct {
ParserOptions
PrinterOptions
}
// `Parse` converts shell script text into a structured syntax tree.
// It assembles parser options based on the provided configuration—such as whether to keep comments,
// the shell syntax variant to use, an optional stopping point, and the desired error recovery level.
// The supplied file path is used for contextual error reporting.
// It returns a syntax.File representing the parsed script, or an error if parsing fails.
func Parse(text string, filepath string, parserOptions ParserOptions) (*syntax.File, error) {
var options []syntax.ParserOption
options = append(options, syntax.KeepComments(parserOptions.KeepComments), syntax.Variant(parserOptions.Variant))
if parserOptions.StopAt != "" {
options = append(options, syntax.StopAt(parserOptions.StopAt))
}
if parserOptions.RecoverErrors != 0 {
options = append(options, syntax.RecoverErrors(parserOptions.RecoverErrors))
}
parser = syntax.NewParser(options...)
return parser.Parse(bytes.NewReader([]byte(text)), filepath)
}
// `Print` returns the formatted shell script defined in originalText.
// It first parses the input using the parser options in syntaxOptions and then prints the resulting
// syntax tree using printer options—including indentation, single-line formatting, and others.
// The filepath parameter is used for context in error messages. On success, Print returns the formatted
// script as a string, or an error if parsing or printing fails.
func Print(originalText string, filepath string, syntaxOptions SyntaxOptions) (string, error) {
file, err := Parse(originalText, filepath, syntaxOptions.ParserOptions)
if err != nil {
return "", err
}
printer = syntax.NewPrinter(
syntax.Indent(syntaxOptions.Indent),
syntax.BinaryNextLine(syntaxOptions.BinaryNextLine),
syntax.SwitchCaseIndent(syntaxOptions.SwitchCaseIndent),
syntax.SpaceRedirects(syntaxOptions.SpaceRedirects),
syntax.KeepPadding(syntaxOptions.KeepPadding),
syntax.Minify(syntaxOptions.Minify),
syntax.SingleLine(syntaxOptions.SingleLine),
syntax.FunctionNextLine(syntaxOptions.FunctionNextLine),
)
var buf bytes.Buffer
writer := io.Writer(&buf)
err = printer.Print(writer, file)
if err != nil {
return "", err
}
return buf.String(), err
}

View File

@@ -0,0 +1,219 @@
package src
import (
"mvdan.cc/sh/v3/syntax"
)
type Pos struct {
Offset uint
Line uint
Col uint
}
type Node struct {
Pos Pos
End Pos
}
type Comment struct {
Hash Pos
Text string
Pos Pos
End Pos
}
type Word struct {
Parts []Node
Lit string
Pos Pos
End Pos
}
type Lit struct {
ValuePos Pos
ValueEnd Pos
Value string
Pos Pos
End Pos
}
type Redirect struct {
OpPos Pos
Op string
N *Lit
Word *Word
Hdoc *Word
Pos Pos
End Pos
}
type Stmt struct {
Comments []Comment
Cmd *Node
Position Pos
Semicolon Pos
Negated bool
Background bool
Coprocess bool
Redirs []Redirect
Pos Pos
End Pos
}
type File struct {
Name string
Stmt []Stmt
Last []Comment
Pos Pos
End Pos
}
type ParseError struct {
syntax.ParseError
Pos Pos
}
type Result struct {
File `json:"file"`
Text string `json:"text"`
*ParseError `json:"parseError"`
Message string `json:"message"`
}
func MapParseError(err error) (*ParseError, string) {
if err == nil {
return nil, ""
}
parseError, ok := err.(syntax.ParseError)
if ok {
return &ParseError{
ParseError: parseError,
Pos: mapPos(parseError.Pos),
}, parseError.Error()
}
return nil, err.Error()
}
func mapPos(pos syntax.Pos) Pos {
return Pos{
Offset: pos.Offset(),
Line: pos.Line(),
Col: pos.Col(),
}
}
func mapNode(node syntax.Node) *Node {
if node == nil {
return nil
}
return &Node{
Pos: mapPos(node.Pos()),
End: mapPos(node.End()),
}
}
// `mapComments` transforms a slice of syntax.Comment into a slice of Comment by converting each comment's hash, text, start, and end positions using mapPos. It preserves the order of the comments and returns an empty slice if the input is nil or empty.
func mapComments(comments []syntax.Comment) []Comment {
commentsSize := len(comments)
commentList := make([]Comment, commentsSize)
for i := range commentsSize {
curr := comments[i]
commentList[i] = Comment{
Hash: mapPos(curr.Hash),
Text: curr.Text,
Pos: mapPos(curr.Pos()),
End: mapPos(curr.End()),
}
}
return commentList
}
// `mapWord` converts a *syntax.Word into a custom *Word structure. It maps each part of the syntax.Word using mapNode,
// extracts the literal via Lit(), and maps the start and end positions using mapPos. If the input word is nil, it returns nil.
func mapWord(word *syntax.Word) *Word {
if word == nil {
return nil
}
size := len(word.Parts)
parts := make([]Node, size)
for i := range size {
parts[i] = *mapNode(word.Parts[i])
}
return &Word{
Parts: parts,
Lit: word.Lit(),
Pos: mapPos(word.Pos()),
End: mapPos(word.End()),
}
}
// `mapRedirects` converts a slice of syntax.Redirect pointers into a slice of custom Redirect structures.
// It maps each redirects operator position, associated literal (if present), word, heredoc, and overall positional data using helper functions.
// If the literal component (N) is non-nil, it is transformed into a Lit structure that encapsulates both its value and positional information.
func mapRedirects(redirects []*syntax.Redirect) []Redirect {
redirsSize := len(redirects)
redirs := make([]Redirect, redirsSize)
for i := range redirsSize {
curr := redirects[i]
var N *Lit
if curr.N != nil {
ValuePos := mapPos(curr.N.Pos())
ValueEnd := mapPos(curr.N.End())
N = &Lit{
ValuePos: ValuePos,
ValueEnd: ValueEnd,
Value: curr.N.Value,
Pos: ValuePos,
End: ValueEnd,
}
}
redirs[i] = Redirect{
OpPos: mapPos(curr.OpPos),
Op: curr.Op.String(),
N: N,
Word: mapWord(curr.Word),
Hdoc: mapWord(curr.Hdoc),
Pos: mapPos(curr.Pos()),
End: mapPos(curr.End()),
}
}
return redirs
}
// `mapStmts` converts a slice of *syntax.Stmt into a slice of Stmt by mapping each statement's components—including comments, command node, positional information, semicolon, redirections, and execution flags (negated, background, coprocess).
func mapStmts(stmts []*syntax.Stmt) []Stmt {
stmtsSize := len(stmts)
stmtList := make([]Stmt, stmtsSize)
for i := range stmtsSize {
curr := stmts[i]
stmtList[i] = Stmt{
Comments: mapComments(curr.Comments),
Cmd: mapNode(curr.Cmd),
Position: mapPos(curr.Position),
Semicolon: mapPos(curr.Semicolon),
Negated: curr.Negated,
Background: curr.Background,
Coprocess: curr.Coprocess,
Redirs: mapRedirects(curr.Redirs),
Pos: mapPos(curr.Pos()),
End: mapPos(curr.End()),
}
}
return stmtList
}
func MapFile(file syntax.File) File {
return File{
Name: file.Name,
Stmt: mapStmts(file.Stmts),
Last: mapComments(file.Last),
Pos: mapPos(file.Pos()),
End: mapPos(file.End()),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,533 @@
// modified based on https://github.com/tinygo-org/tinygo/blob/3e60eeb368f25f237a512e7553fd6d70f36dc74c/targets/wasm_exec.js
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// This file has been modified for use by the TinyGo compiler.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
global.fs = require("fs");
}
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!global.fs) {
let outputBuf = "";
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!global.crypto) {
const nodeCrypto = require("crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
if (!global.TextEncoder) {
global.TextEncoder = require("util").TextEncoder;
}
if (!global.TextDecoder) {
global.TextDecoder = require("util").TextDecoder;
}
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
global.ShellGo = class {
constructor() {
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.memory.buffer);
}
const unboxValue = (v_ref) => {
reinterpretBuf.setBigInt64(0, v_ref, true);
const f = reinterpretBuf.getFloat64(0, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = v_ref & 0xffffffffn;
return this._values[id];
}
const loadValue = (addr) => {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) {
return nanHead << 32n;
}
if (v === 0) {
return (nanHead << 32n) | 1n;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
}
switch (v) {
case undefined:
return 0n;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = BigInt(this._values.length);
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 1n;
switch (typeof v) {
case "string":
typeFlag = 2n;
break;
case "symbol":
typeFlag = 3n;
break;
case "function":
typeFlag = 4n;
break;
}
return id | ((nanHead | typeFlag) << 32n);
}
const storeValue = (addr, v) => {
let v_ref = boxValue(v);
mem().setBigUint64(addr, v_ref, true);
}
const loadSlice = (array, len, cap) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
}
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (ptr, len) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
wasi_snapshot_preview1: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
},
gojs: {
// func ticks() float64
"runtime.ticks": () => {
return timeOrigin + performance.now();
},
// func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(() => {
if (this.exited) return;
try {
this._inst.exports.go_scheduler();
} catch (e) {
if (e !== wasmExit) throw e;
}
}, timeout);
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (v_ref) => {
// Note: TinyGo does not support finalizers so this is only called
// for one specific case, by js.go:jsString. and can/might leak memory.
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (value_ptr, value_len) => {
value_ptr >>>= 0;
const s = loadString(value_ptr, value_len);
return boxValue(s);
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
let prop = loadString(p_ptr, p_len);
let v = unboxValue(v_ref);
let result = Reflect.get(v, prop);
return boxValue(result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (v_ref, i) => {
return boxValue(Reflect.get(unboxValue(v_ref), i));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const m = Reflect.get(v, name);
storeValue(ret_addr, Reflect.apply(m, v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
try {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
storeValue(ret_addr, Reflect.apply(v, undefined, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
storeValue(ret_addr, Reflect.construct(v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr+ 8, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (v_ref) => {
return unboxValue(v_ref).length;
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
const s = String(unboxValue(v_ref));
const str = encoder.encode(s);
storeValue(ret_addr, str);
mem().setInt32(ret_addr + 8, str.length, true);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
const str = unboxValue(v_ref);
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
return unboxValue(v_ref) instanceof unboxValue(t_ref);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
// copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified:
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = unboxValue(dst_ref);
const src = loadSlice(src_addr, src_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
}
};
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
// For compatibility, we use both as long as Go 1.20 is supported.
this.importObject.env = this.importObject.gojs;
}
async run(instance) {
this._inst = instance;
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
global,
this,
];
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this.exitCode = 0;
if (this._inst.exports._start) {
let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});
// Run program, but catch the wasmExit exception that's thrown
// to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}
await exitPromise;
return this.exitCode;
} else {
this._inst.exports._initialize();
}
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
try {
this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

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