Compare commits
7 Commits
v1.3.5
...
d24a522b32
| Author | SHA1 | Date | |
|---|---|---|---|
| d24a522b32 | |||
| 41afb834ae | |||
| b745329e26 | |||
| 1fb4f64cb3 | |||
| 1f8e8981ce | |||
| a257d30dba | |||
| 97ee3b0667 |
@@ -0,0 +1,64 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service represents the notifications service
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as application$0 from "../../application/models.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RemoveBadge removes the badge label from the application icon.
|
||||||
|
*/
|
||||||
|
export function RemoveBadge(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2374916939) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceName returns the name of the service.
|
||||||
|
*/
|
||||||
|
export function ServiceName(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2428202016) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceShutdown is called when the service is unloaded.
|
||||||
|
*/
|
||||||
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3893755233) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceStartup is called when the service is loaded.
|
||||||
|
*/
|
||||||
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(4078800764, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetBadge sets the badge label on the application icon.
|
||||||
|
*/
|
||||||
|
export function SetBadge(label: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(784276339, label) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetCustomBadge(label: string, options: $models.Options): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3058653106, label, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
import * as BadgeService from "./badgeservice.js";
|
||||||
|
export {
|
||||||
|
BadgeService
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as color$0 from "../../../../../../../image/color/models.js";
|
||||||
|
|
||||||
|
export class Options {
|
||||||
|
"TextColour": color$0.RGBA;
|
||||||
|
"BackgroundColour": color$0.RGBA;
|
||||||
|
"FontName": string;
|
||||||
|
"FontSize": number;
|
||||||
|
"SmallFontSize": number;
|
||||||
|
|
||||||
|
/** Creates a new Options instance. */
|
||||||
|
constructor($$source: Partial<Options> = {}) {
|
||||||
|
if (!("TextColour" in $$source)) {
|
||||||
|
this["TextColour"] = (new color$0.RGBA());
|
||||||
|
}
|
||||||
|
if (!("BackgroundColour" in $$source)) {
|
||||||
|
this["BackgroundColour"] = (new color$0.RGBA());
|
||||||
|
}
|
||||||
|
if (!("FontName" in $$source)) {
|
||||||
|
this["FontName"] = "";
|
||||||
|
}
|
||||||
|
if (!("FontSize" in $$source)) {
|
||||||
|
this["FontSize"] = 0;
|
||||||
|
}
|
||||||
|
if (!("SmallFontSize" in $$source)) {
|
||||||
|
this["SmallFontSize"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Options instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): Options {
|
||||||
|
const $$createField0_0 = $$createType0;
|
||||||
|
const $$createField1_0 = $$createType0;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("TextColour" in $$parsedSource) {
|
||||||
|
$$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]);
|
||||||
|
}
|
||||||
|
if ("BackgroundColour" in $$parsedSource) {
|
||||||
|
$$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]);
|
||||||
|
}
|
||||||
|
return new Options($$parsedSource as Partial<Options>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = color$0.RGBA.createFrom;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
import * as NotificationService from "./notificationservice.js";
|
||||||
|
export {
|
||||||
|
NotificationService
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotificationAction represents an action button for a notification.
|
||||||
|
*/
|
||||||
|
export class NotificationAction {
|
||||||
|
"id"?: string;
|
||||||
|
"title"?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (macOS-specific)
|
||||||
|
*/
|
||||||
|
"destructive"?: boolean;
|
||||||
|
|
||||||
|
/** Creates a new NotificationAction instance. */
|
||||||
|
constructor($$source: Partial<NotificationAction> = {}) {
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new NotificationAction instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): NotificationAction {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotificationCategory groups actions for notifications.
|
||||||
|
*/
|
||||||
|
export class NotificationCategory {
|
||||||
|
"id"?: string;
|
||||||
|
"actions"?: NotificationAction[];
|
||||||
|
"hasReplyField"?: boolean;
|
||||||
|
"replyPlaceholder"?: string;
|
||||||
|
"replyButtonTitle"?: string;
|
||||||
|
|
||||||
|
/** Creates a new NotificationCategory instance. */
|
||||||
|
constructor($$source: Partial<NotificationCategory> = {}) {
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new NotificationCategory instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): NotificationCategory {
|
||||||
|
const $$createField1_0 = $$createType1;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("actions" in $$parsedSource) {
|
||||||
|
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
|
||||||
|
}
|
||||||
|
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotificationOptions contains configuration for a notification
|
||||||
|
*/
|
||||||
|
export class NotificationOptions {
|
||||||
|
"id": string;
|
||||||
|
"title": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (macOS and Linux only)
|
||||||
|
*/
|
||||||
|
"subtitle"?: string;
|
||||||
|
"body"?: string;
|
||||||
|
"categoryId"?: string;
|
||||||
|
"data"?: { [_: string]: any };
|
||||||
|
|
||||||
|
/** Creates a new NotificationOptions instance. */
|
||||||
|
constructor($$source: Partial<NotificationOptions> = {}) {
|
||||||
|
if (!("id" in $$source)) {
|
||||||
|
this["id"] = "";
|
||||||
|
}
|
||||||
|
if (!("title" in $$source)) {
|
||||||
|
this["title"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new NotificationOptions instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): NotificationOptions {
|
||||||
|
const $$createField5_0 = $$createType2;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("data" in $$parsedSource) {
|
||||||
|
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
|
||||||
|
}
|
||||||
|
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private type creation functions
|
||||||
|
const $$createType0 = NotificationAction.createFrom;
|
||||||
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
|
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service represents the notifications service
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as application$0 from "../../application/models.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
export function CheckNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2216952893) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnNotificationResponse registers a callback function that will be called when
|
||||||
|
* a notification response is received from the user.
|
||||||
|
*/
|
||||||
|
export function OnNotificationResponse(callback: any): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1642697808, callback) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterNotificationCategory(category: $models.NotificationCategory): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2917562919, category) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllDeliveredNotifications(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3956282340) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllPendingNotifications(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(108821341) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveDeliveredNotification(identifier: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(975691940, identifier) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotification(identifier: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3966653866, identifier) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotificationCategory(categoryID: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2032615554, categoryID) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemovePendingNotification(identifier: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3729049703, identifier) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public methods that delegate to the implementation.
|
||||||
|
*/
|
||||||
|
export function RequestNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3933442950) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotification(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3968228732, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotificationWithActions(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1886542847, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceName returns the name of the service.
|
||||||
|
*/
|
||||||
|
export function ServiceName(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2704532675) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceShutdown is called when the service is unloaded.
|
||||||
|
*/
|
||||||
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2550195434) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceStartup is called when the service is loaded.
|
||||||
|
*/
|
||||||
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(4047820929, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
4
frontend/bindings/image/color/index.ts
Normal file
4
frontend/bindings/image/color/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
46
frontend/bindings/image/color/models.ts
Normal file
46
frontend/bindings/image/color/models.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RGBA represents a traditional 32-bit alpha-premultiplied color, having 8
|
||||||
|
* bits for each of red, green, blue and alpha.
|
||||||
|
*
|
||||||
|
* An alpha-premultiplied color component C has been scaled by alpha (A), so
|
||||||
|
* has valid values 0 <= C <= A.
|
||||||
|
*/
|
||||||
|
export class RGBA {
|
||||||
|
"R": number;
|
||||||
|
"G": number;
|
||||||
|
"B": number;
|
||||||
|
"A": number;
|
||||||
|
|
||||||
|
/** Creates a new RGBA instance. */
|
||||||
|
constructor($$source: Partial<RGBA> = {}) {
|
||||||
|
if (!("R" in $$source)) {
|
||||||
|
this["R"] = 0;
|
||||||
|
}
|
||||||
|
if (!("G" in $$source)) {
|
||||||
|
this["G"] = 0;
|
||||||
|
}
|
||||||
|
if (!("B" in $$source)) {
|
||||||
|
this["B"] = 0;
|
||||||
|
}
|
||||||
|
if (!("A" in $$source)) {
|
||||||
|
this["A"] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new RGBA instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): RGBA {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new RGBA($$parsedSource as Partial<RGBA>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,14 @@ export function GetSettingsPath(): Promise<string> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MigrateConfig 执行配置迁移
|
||||||
|
*/
|
||||||
|
export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(434292783) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResetConfig 强制重置所有配置为默认值
|
* ResetConfig 强制重置所有配置为默认值
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import * as MigrationService from "./migrationservice.js";
|
|||||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||||
import * as StartupService from "./startupservice.js";
|
import * as StartupService from "./startupservice.js";
|
||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
|
import * as TestService from "./testservice.js";
|
||||||
import * as ThemeService from "./themeservice.js";
|
import * as ThemeService from "./themeservice.js";
|
||||||
import * as TranslationService from "./translationservice.js";
|
import * as TranslationService from "./translationservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
@@ -31,6 +32,7 @@ export {
|
|||||||
SelfUpdateService,
|
SelfUpdateService,
|
||||||
StartupService,
|
StartupService,
|
||||||
SystemService,
|
SystemService,
|
||||||
|
TestService,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
TranslationService,
|
TranslationService,
|
||||||
TrayService,
|
TrayService,
|
||||||
|
|||||||
55
frontend/bindings/voidraft/internal/services/testservice.ts
Normal file
55
frontend/bindings/voidraft/internal/services/testservice.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestService 测试服务 - 仅在开发环境使用
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClearAll 清除所有测试状态
|
||||||
|
*/
|
||||||
|
export function ClearAll(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2179720854) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceStartup 服务启动时调用
|
||||||
|
*/
|
||||||
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(617408198, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestBadge 测试Badge功能
|
||||||
|
*/
|
||||||
|
export function TestBadge(text: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(4242952145, text) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestNotification 测试通知功能
|
||||||
|
*/
|
||||||
|
export function TestNotification(title: string, subtitle: string, body: string): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1697553289, title, subtitle, body) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestUpdateNotification 测试更新通知
|
||||||
|
*/
|
||||||
|
export function TestUpdateNotification(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3091730060) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
2291
frontend/package-lock.json
generated
2291
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,13 @@
|
|||||||
"lint:fix": "eslint --fix"
|
"lint:fix": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.7",
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-angular": "^0.1.4",
|
"@codemirror/lang-angular": "^0.1.4",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-go": "^6.0.1",
|
"@codemirror/lang-go": "^6.0.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.10",
|
||||||
"@codemirror/lang-java": "^6.0.2",
|
"@codemirror/lang-java": "^6.0.2",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"@codemirror/lang-sql": "^6.9.1",
|
"@codemirror/lang-sql": "^6.9.1",
|
||||||
"@codemirror/lang-vue": "^0.1.3",
|
"@codemirror/lang-vue": "^0.1.3",
|
||||||
"@codemirror/lang-wast": "^6.0.2",
|
"@codemirror/lang-wast": "^6.0.2",
|
||||||
"@codemirror/lang-xml": "^6.1.0",
|
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.3",
|
"@codemirror/language": "^6.11.3",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
"@codemirror/lint": "^6.8.5",
|
"@codemirror/lint": "^6.8.5",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.1",
|
"@codemirror/view": "^6.38.2",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
@@ -50,31 +49,37 @@
|
|||||||
"colors-named-hex": "^1.0.2",
|
"colors-named-hex": "^1.0.2",
|
||||||
"franc-min": "^6.2.0",
|
"franc-min": "^6.2.0",
|
||||||
"hsl-matcher": "^1.2.4",
|
"hsl-matcher": "^1.2.4",
|
||||||
|
"jsox": "^1.2.123",
|
||||||
"lezer": "^0.13.5",
|
"lezer": "^0.13.5",
|
||||||
|
"linguist-languages": "^9.0.0",
|
||||||
|
"node-sql-parser": "^5.3.12",
|
||||||
|
"php-parser": "^3.2.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"remarkable": "^2.0.1",
|
"remarkable": "^2.0.1",
|
||||||
"sass": "^1.90.0",
|
"sass": "^1.92.1",
|
||||||
"vue": "^3.5.19",
|
"sql-formatter": "^15.6.9",
|
||||||
"vue-i18n": "^11.1.11",
|
"vue": "^3.5.21",
|
||||||
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-pick-colors": "^1.8.0",
|
"vue-pick-colors": "^1.8.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.34.0",
|
"@eslint/js": "^9.35.0",
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.1",
|
||||||
"@types/remarkable": "^2.0.8",
|
"@types/remarkable": "^2.0.8",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "latest",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.35.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.4.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.40.0",
|
"typescript-eslint": "^8.43.0",
|
||||||
"unplugin-vue-components": "^29.0.0",
|
"unplugin-vue-components": "^29.0.0",
|
||||||
"vite": "^7.1.3",
|
"vite": "^7.1.5",
|
||||||
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.0.6"
|
"vue-tsc": "^3.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
frontend/public/go.wasm
Normal file
BIN
frontend/public/go.wasm
Normal file
Binary file not shown.
561
frontend/public/wasm_exec.js
Normal file
561
frontend/public/wasm_exec.js
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const enosys = () => {
|
||||||
|
const err = new Error("not implemented");
|
||||||
|
err.code = "ENOSYS";
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!globalThis.fs) {
|
||||||
|
let outputBuf = "";
|
||||||
|
globalThis.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.substring(0, nl));
|
||||||
|
outputBuf = outputBuf.substring(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 (!globalThis.process) {
|
||||||
|
globalThis.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 (!globalThis.crypto) {
|
||||||
|
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.performance) {
|
||||||
|
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextEncoder) {
|
||||||
|
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.TextDecoder) {
|
||||||
|
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder("utf-8");
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
|
globalThis.Go = class {
|
||||||
|
constructor() {
|
||||||
|
this.argv = ["js"];
|
||||||
|
this.env = {};
|
||||||
|
this.exit = (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.warn("exit code:", code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._exitPromise = new Promise((resolve) => {
|
||||||
|
this._resolveExitPromise = resolve;
|
||||||
|
});
|
||||||
|
this._pendingEvent = null;
|
||||||
|
this._scheduledTimeouts = new Map();
|
||||||
|
this._nextCallbackTimeoutID = 1;
|
||||||
|
|
||||||
|
const setInt64 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInt32 = (addr, v) => {
|
||||||
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInt64 = (addr) => {
|
||||||
|
const low = this.mem.getUint32(addr + 0, true);
|
||||||
|
const high = this.mem.getInt32(addr + 4, true);
|
||||||
|
return low + high * 4294967296;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadValue = (addr) => {
|
||||||
|
const f = this.mem.getFloat64(addr, true);
|
||||||
|
if (f === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!isNaN(f)) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = this.mem.getUint32(addr, true);
|
||||||
|
return this._values[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeValue = (addr, v) => {
|
||||||
|
const nanHead = 0x7FF80000;
|
||||||
|
|
||||||
|
if (typeof v === "number" && v !== 0) {
|
||||||
|
if (isNaN(v)) {
|
||||||
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
|
this.mem.setUint32(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mem.setFloat64(addr, v, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === undefined) {
|
||||||
|
this.mem.setFloat64(addr, 0, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = this._ids.get(v);
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._idPool.pop();
|
||||||
|
if (id === undefined) {
|
||||||
|
id = this._values.length;
|
||||||
|
}
|
||||||
|
this._values[id] = v;
|
||||||
|
this._goRefCounts[id] = 0;
|
||||||
|
this._ids.set(v, id);
|
||||||
|
}
|
||||||
|
this._goRefCounts[id]++;
|
||||||
|
let typeFlag = 0;
|
||||||
|
switch (typeof v) {
|
||||||
|
case "object":
|
||||||
|
if (v !== null) {
|
||||||
|
typeFlag = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
typeFlag = 2;
|
||||||
|
break;
|
||||||
|
case "symbol":
|
||||||
|
typeFlag = 3;
|
||||||
|
break;
|
||||||
|
case "function":
|
||||||
|
typeFlag = 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||||
|
this.mem.setUint32(addr, id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlice = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSliceOfValues = (addr) => {
|
||||||
|
const array = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
const a = new Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
a[i] = loadValue(array + i * 8);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadString = (addr) => {
|
||||||
|
const saddr = getInt64(addr + 0);
|
||||||
|
const len = getInt64(addr + 8);
|
||||||
|
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeOrigin = Date.now() - performance.now();
|
||||||
|
this.importObject = {
|
||||||
|
_gotest: {
|
||||||
|
add: (a, b) => a + b,
|
||||||
|
},
|
||||||
|
gojs: {
|
||||||
|
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||||
|
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||||
|
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||||
|
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||||
|
|
||||||
|
// func wasmExit(code int32)
|
||||||
|
"runtime.wasmExit": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const code = this.mem.getInt32(sp + 8, true);
|
||||||
|
this.exited = true;
|
||||||
|
delete this._inst;
|
||||||
|
delete this._values;
|
||||||
|
delete this._goRefCounts;
|
||||||
|
delete this._ids;
|
||||||
|
delete this._idPool;
|
||||||
|
this.exit(code);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||||
|
"runtime.wasmWrite": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const fd = getInt64(sp + 8);
|
||||||
|
const p = getInt64(sp + 16);
|
||||||
|
const n = this.mem.getInt32(sp + 24, true);
|
||||||
|
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func resetMemoryDataView()
|
||||||
|
"runtime.resetMemoryDataView": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func nanotime1() int64
|
||||||
|
"runtime.nanotime1": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func walltime() (sec int64, nsec int32)
|
||||||
|
"runtime.walltime": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const msec = (new Date).getTime();
|
||||||
|
setInt64(sp + 8, msec / 1000);
|
||||||
|
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func scheduleTimeoutEvent(delay int64) int32
|
||||||
|
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this._nextCallbackTimeoutID;
|
||||||
|
this._nextCallbackTimeoutID++;
|
||||||
|
this._scheduledTimeouts.set(id, setTimeout(
|
||||||
|
() => {
|
||||||
|
this._resume();
|
||||||
|
while (this._scheduledTimeouts.has(id)) {
|
||||||
|
// for some reason Go failed to register the timeout event, log and try again
|
||||||
|
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||||
|
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInt64(sp + 8),
|
||||||
|
));
|
||||||
|
this.mem.setInt32(sp + 16, id, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func clearTimeoutEvent(id int32)
|
||||||
|
"runtime.clearTimeoutEvent": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getInt32(sp + 8, true);
|
||||||
|
clearTimeout(this._scheduledTimeouts.get(id));
|
||||||
|
this._scheduledTimeouts.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func getRandomData(r []byte)
|
||||||
|
"runtime.getRandomData": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
crypto.getRandomValues(loadSlice(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func finalizeRef(v ref)
|
||||||
|
"syscall/js.finalizeRef": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const id = this.mem.getUint32(sp + 8, true);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func stringVal(value string) ref
|
||||||
|
"syscall/js.stringVal": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, loadString(sp + 8));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueGet(v ref, p string) ref
|
||||||
|
"syscall/js.valueGet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 32, result);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueSet(v ref, p string, x ref)
|
||||||
|
"syscall/js.valueSet": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueDelete(v ref, p string)
|
||||||
|
"syscall/js.valueDelete": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueIndex(v ref, i int) ref
|
||||||
|
"syscall/js.valueIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueSetIndex(v ref, i int, x ref)
|
||||||
|
"syscall/js.valueSetIndex": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueCall": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const m = Reflect.get(v, loadString(sp + 16));
|
||||||
|
const args = loadSliceOfValues(sp + 32);
|
||||||
|
const result = Reflect.apply(m, v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, result);
|
||||||
|
this.mem.setUint8(sp + 64, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 56, err);
|
||||||
|
this.mem.setUint8(sp + 64, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueInvoke": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.apply(v, undefined, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueNew(v ref, args []ref) (ref, bool)
|
||||||
|
"syscall/js.valueNew": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
try {
|
||||||
|
const v = loadValue(sp + 8);
|
||||||
|
const args = loadSliceOfValues(sp + 16);
|
||||||
|
const result = Reflect.construct(v, args);
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, result);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
} catch (err) {
|
||||||
|
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||||
|
storeValue(sp + 40, err);
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueLength(v ref) int
|
||||||
|
"syscall/js.valueLength": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||||
|
},
|
||||||
|
|
||||||
|
// valuePrepareString(v ref) (ref, int)
|
||||||
|
"syscall/js.valuePrepareString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||||
|
storeValue(sp + 16, str);
|
||||||
|
setInt64(sp + 24, str.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
// valueLoadString(v ref, b []byte)
|
||||||
|
"syscall/js.valueLoadString": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const str = loadValue(sp + 8);
|
||||||
|
loadSlice(sp + 16).set(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func valueInstanceOf(v ref, t ref) bool
|
||||||
|
"syscall/js.valueInstanceOf": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||||
|
"syscall/js.copyBytesToGo": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadSlice(sp + 8);
|
||||||
|
const src = loadValue(sp + 32);
|
||||||
|
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||||
|
"syscall/js.copyBytesToJS": (sp) => {
|
||||||
|
sp >>>= 0;
|
||||||
|
const dst = loadValue(sp + 8);
|
||||||
|
const src = loadSlice(sp + 16);
|
||||||
|
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||||
|
this.mem.setUint8(sp + 48, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toCopy = src.subarray(0, dst.length);
|
||||||
|
dst.set(toCopy);
|
||||||
|
setInt64(sp + 40, toCopy.length);
|
||||||
|
this.mem.setUint8(sp + 48, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": (value) => {
|
||||||
|
console.log(value);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(instance) {
|
||||||
|
if (!(instance instanceof WebAssembly.Instance)) {
|
||||||
|
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||||
|
}
|
||||||
|
this._inst = instance;
|
||||||
|
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||||
|
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||||
|
NaN,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
globalThis,
|
||||||
|
this,
|
||||||
|
];
|
||||||
|
this._goRefCounts = new Array(this._values.length).fill(Infinity); // 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
|
||||||
|
[0, 1],
|
||||||
|
[null, 2],
|
||||||
|
[true, 3],
|
||||||
|
[false, 4],
|
||||||
|
[globalThis, 5],
|
||||||
|
[this, 6],
|
||||||
|
]);
|
||||||
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
|
this.exited = false; // whether the Go program has exited
|
||||||
|
|
||||||
|
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||||
|
let offset = 4096;
|
||||||
|
|
||||||
|
const strPtr = (str) => {
|
||||||
|
const ptr = offset;
|
||||||
|
const bytes = encoder.encode(str + "\0");
|
||||||
|
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||||
|
offset += bytes.length;
|
||||||
|
if (offset % 8 !== 0) {
|
||||||
|
offset += 8 - (offset % 8);
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const argc = this.argv.length;
|
||||||
|
|
||||||
|
const argvPtrs = [];
|
||||||
|
this.argv.forEach((arg) => {
|
||||||
|
argvPtrs.push(strPtr(arg));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const keys = Object.keys(this.env).sort();
|
||||||
|
keys.forEach((key) => {
|
||||||
|
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||||
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
|
const argv = offset;
|
||||||
|
argvPtrs.forEach((ptr) => {
|
||||||
|
this.mem.setUint32(offset, ptr, true);
|
||||||
|
this.mem.setUint32(offset + 4, 0, true);
|
||||||
|
offset += 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||||
|
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||||
|
const wasmMinDataAddr = 4096 + 8192;
|
||||||
|
if (offset >= wasmMinDataAddr) {
|
||||||
|
throw new Error("total length of command line and environment variables exceeds limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._inst.exports.run(argc, argv);
|
||||||
|
if (this.exited) {
|
||||||
|
this._resolveExitPromise();
|
||||||
|
}
|
||||||
|
await this._exitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resume() {
|
||||||
|
if (this.exited) {
|
||||||
|
throw new Error("Go program has already exited");
|
||||||
|
}
|
||||||
|
this._inst.exports.resume();
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -8,19 +8,11 @@ import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
|||||||
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
||||||
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
||||||
import BackupPage from '@/views/settings/pages/BackupPage.vue';
|
import BackupPage from '@/views/settings/pages/BackupPage.vue';
|
||||||
|
// 测试页面
|
||||||
|
import TestPage from '@/views/settings/pages/TestPage.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
// 基础设置子路由
|
||||||
{
|
const settingsChildren: RouteRecordRaw[] = [
|
||||||
path: '/',
|
|
||||||
name: 'Editor',
|
|
||||||
component: Editor
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
redirect: '/settings/general',
|
|
||||||
component: Settings,
|
|
||||||
children: [
|
|
||||||
{
|
{
|
||||||
path: 'general',
|
path: 'general',
|
||||||
name: 'SettingsGeneral',
|
name: 'SettingsGeneral',
|
||||||
@@ -56,7 +48,29 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'SettingsBackup',
|
name: 'SettingsBackup',
|
||||||
component: BackupPage
|
component: BackupPage
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
|
// 仅在开发环境添加测试页面路由
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
settingsChildren.push({
|
||||||
|
path: 'test',
|
||||||
|
name: 'SettingsTest',
|
||||||
|
component: TestPage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Editor',
|
||||||
|
component: Editor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
redirect: '/settings/general',
|
||||||
|
component: Settings,
|
||||||
|
children: settingsChildren
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
32
frontend/src/utils/prettier/plugins/go/build.bat
Normal file
32
frontend/src/utils/prettier/plugins/go/build.bat
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
rem Build script for Go Prettier Plugin WASM
|
||||||
|
rem This script compiles the Go code to WebAssembly
|
||||||
|
|
||||||
|
echo 🔨 Building Go Prettier Plugin WASM...
|
||||||
|
|
||||||
|
rem Set WASM build environment
|
||||||
|
set GOOS=js
|
||||||
|
set GOARCH=wasm
|
||||||
|
|
||||||
|
rem Build the WASM file
|
||||||
|
echo Compiling main.go to go.wasm...
|
||||||
|
go build -o go.wasm main.go
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 0 (
|
||||||
|
echo ✅ Build successful!
|
||||||
|
|
||||||
|
rem Show file size (Windows version)
|
||||||
|
for %%A in (go.wasm) do echo 📊 WASM file size: %%~zA bytes
|
||||||
|
|
||||||
|
rem Copy to public directory for browser access
|
||||||
|
if exist "..\..\..\..\..\public" (
|
||||||
|
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
|
||||||
|
echo 📋 Copied to public directory
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 🎉 Go Prettier Plugin WASM is ready!
|
||||||
|
) else (
|
||||||
|
echo ❌ Build failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
30
frontend/src/utils/prettier/plugins/go/build.sh
Normal file
30
frontend/src/utils/prettier/plugins/go/build.sh
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build script for Go Prettier Plugin WASM
|
||||||
|
# This script compiles the Go code to WebAssembly
|
||||||
|
|
||||||
|
echo "🔨 Building Go Prettier Plugin WASM..."
|
||||||
|
|
||||||
|
# Set WASM build environment
|
||||||
|
export GOOS=js
|
||||||
|
export GOARCH=wasm
|
||||||
|
|
||||||
|
# Build the WASM file
|
||||||
|
echo "Compiling main.go to go.wasm..."
|
||||||
|
go build -o go.wasm main.go
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ Build successful!"
|
||||||
|
echo "📊 WASM file size: $(du -h go.wasm | cut -f1)"
|
||||||
|
|
||||||
|
# Copy to public directory for browser access
|
||||||
|
if [ -d "../../../../../public" ]; then
|
||||||
|
cp go.wasm ../../../../../public/go.wasm
|
||||||
|
echo "📋 Copied to public directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Go Prettier Plugin WASM is ready!"
|
||||||
|
else
|
||||||
|
echo "❌ Build failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
10
frontend/src/utils/prettier/plugins/go/go.d.ts
vendored
Normal file
10
frontend/src/utils/prettier/plugins/go/go.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Parser, Plugin } from "prettier";
|
||||||
|
|
||||||
|
export declare const languages: Plugin["languages"];
|
||||||
|
export declare const parsers: {
|
||||||
|
go: Parser;
|
||||||
|
};
|
||||||
|
export declare const printers: Plugin["printers"];
|
||||||
|
|
||||||
|
declare const plugin: Plugin;
|
||||||
|
export default plugin;
|
||||||
116
frontend/src/utils/prettier/plugins/go/go.mjs
Normal file
116
frontend/src/utils/prettier/plugins/go/go.mjs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Go Prettier Plugin for Vite + Vue3 Environment
|
||||||
|
* WebAssembly-based Go code formatter for Prettier
|
||||||
|
*/
|
||||||
|
|
||||||
|
let initializePromise = null;
|
||||||
|
|
||||||
|
// Load WASM file from public directory
|
||||||
|
const loadWasm = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/go.wasm');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return await response.arrayBuffer();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WASM loading failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Go runtime
|
||||||
|
const initGoRuntime = async () => {
|
||||||
|
if (globalThis.Go) return;
|
||||||
|
|
||||||
|
// Auto-load wasm_exec.js if not available
|
||||||
|
try {
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = '/wasm_exec.js';
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = () => reject(new Error('Failed to load wasm_exec.js'));
|
||||||
|
setTimeout(() => reject(new Error('wasm_exec.js loading timeout')), 5000);
|
||||||
|
});
|
||||||
|
if (!globalThis.Go) {
|
||||||
|
throw new Error('Go WASM runtime is not available after loading wasm_exec.js');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load wasm_exec.js:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
if (initializePromise) return initializePromise;
|
||||||
|
|
||||||
|
initializePromise = (async () => {
|
||||||
|
await initGoRuntime();
|
||||||
|
|
||||||
|
const go = new globalThis.Go();
|
||||||
|
const wasmBuffer = await loadWasm();
|
||||||
|
const {instance} = await WebAssembly.instantiate(wasmBuffer, go.importObject);
|
||||||
|
|
||||||
|
// Run Go program
|
||||||
|
go.run(instance).catch(err => {
|
||||||
|
console.error('Go WASM program exit error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initialization to complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
// Check if formatGo function is available
|
||||||
|
if (typeof globalThis.formatGo !== 'function') {
|
||||||
|
throw new Error('Go WASM module not properly initialized - formatGo function not available');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return initializePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const languages = [
|
||||||
|
{
|
||||||
|
name: "Go",
|
||||||
|
parsers: ["go"],
|
||||||
|
extensions: [".go"],
|
||||||
|
vscodeLanguageIds: ["go"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const parsers = {
|
||||||
|
go: {
|
||||||
|
parse: (text) => text,
|
||||||
|
astFormat: "go-format",
|
||||||
|
locStart: (node) => 0,
|
||||||
|
locEnd: (node) => node.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printers = {
|
||||||
|
"go-format": {
|
||||||
|
print: async (path) => {
|
||||||
|
await initialize();
|
||||||
|
const text = path.getValue();
|
||||||
|
|
||||||
|
if (typeof globalThis.formatGo !== 'function') {
|
||||||
|
throw new Error('Go WASM module not properly initialized - formatGo function missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return globalThis.formatGo(text);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Go formatting failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default export for Prettier plugin compatibility
|
||||||
|
export default {
|
||||||
|
languages,
|
||||||
|
parsers,
|
||||||
|
printers
|
||||||
|
};
|
||||||
255
frontend/src/utils/prettier/plugins/go/main.go
Normal file
255
frontend/src/utils/prettier/plugins/go/main.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
// Package main implements a WebAssembly module that provides Go code formatting
|
||||||
|
// functionality for the Prettier plugin. This package exposes the formatGo function
|
||||||
|
// to JavaScript, enabling web-based Go code formatting with better error tolerance.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"strings"
|
||||||
|
"syscall/js"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatGoCode attempts to format Go source code with better error tolerance
|
||||||
|
func formatGoCode(src string) (string, error) {
|
||||||
|
// Trim input but preserve leading/trailing newlines structure
|
||||||
|
trimmed := strings.TrimSpace(src)
|
||||||
|
if trimmed == "" {
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try the standard format.Source for complete, valid code
|
||||||
|
if formatted, err := format.Source([]byte(src)); err == nil {
|
||||||
|
return string(formatted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new file set for parsing
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
|
||||||
|
// Strategy 1: Try as complete Go file
|
||||||
|
if parsed, err := parser.ParseFile(fset, "", src, parser.ParseComments); err == nil {
|
||||||
|
return formatASTNode(fset, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Try wrapping as package-level declarations
|
||||||
|
packageWrapped := fmt.Sprintf("package main\n\n%s", trimmed)
|
||||||
|
if parsed, err := parser.ParseFile(fset, "", packageWrapped, parser.ParseComments); err == nil {
|
||||||
|
if formatted, err := formatASTNode(fset, parsed); err == nil {
|
||||||
|
return extractPackageContent(formatted), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Try wrapping in main function
|
||||||
|
funcWrapped := fmt.Sprintf("package main\n\nfunc main() {\n%s\n}", indentLines(trimmed, "\t"))
|
||||||
|
if parsed, err := parser.ParseFile(fset, "", funcWrapped, parser.ParseComments); err == nil {
|
||||||
|
if formatted, err := formatASTNode(fset, parsed); err == nil {
|
||||||
|
return extractFunctionBody(formatted), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: Try wrapping in anonymous function
|
||||||
|
anonWrapped := fmt.Sprintf("package main\n\nvar _ = func() {\n%s\n}", indentLines(trimmed, "\t"))
|
||||||
|
if parsed, err := parser.ParseFile(fset, "", anonWrapped, parser.ParseComments); err == nil {
|
||||||
|
if formatted, err := formatASTNode(fset, parsed); err == nil {
|
||||||
|
return extractFunctionBody(formatted), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 5: Try line-by-line formatting for complex cases
|
||||||
|
return formatLineByLine(trimmed, fset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatASTNode formats an AST node using the standard formatter
|
||||||
|
func formatASTNode(fset *token.FileSet, node interface{}) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := format.Node(&buf, fset, node); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPackageContent extracts content after package declaration
|
||||||
|
func extractPackageContent(formatted string) string {
|
||||||
|
lines := strings.Split(formatted, "\n")
|
||||||
|
var contentLines []string
|
||||||
|
skipNext := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "package ") {
|
||||||
|
skipNext = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if skipNext && strings.TrimSpace(line) == "" {
|
||||||
|
skipNext = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !skipNext {
|
||||||
|
contentLines = append(contentLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(contentLines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFunctionBody extracts content from within a function body
|
||||||
|
func extractFunctionBody(formatted string) string {
|
||||||
|
lines := strings.Split(formatted, "\n")
|
||||||
|
var bodyLines []string
|
||||||
|
inFunction := false
|
||||||
|
braceCount := 0
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "func ") && strings.Contains(line, "{") {
|
||||||
|
inFunction = true
|
||||||
|
braceCount = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inFunction {
|
||||||
|
// Count braces to know when function ends
|
||||||
|
braceCount += strings.Count(line, "{")
|
||||||
|
braceCount -= strings.Count(line, "}")
|
||||||
|
|
||||||
|
if braceCount == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove one level of indentation and add the line
|
||||||
|
if strings.HasPrefix(line, "\t") {
|
||||||
|
bodyLines = append(bodyLines, line[1:])
|
||||||
|
} else {
|
||||||
|
bodyLines = append(bodyLines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty lines from start and end
|
||||||
|
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[0]) == "" {
|
||||||
|
bodyLines = bodyLines[1:]
|
||||||
|
}
|
||||||
|
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[len(bodyLines)-1]) == "" {
|
||||||
|
bodyLines = bodyLines[:len(bodyLines)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(bodyLines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// indentLines adds indentation to each non-empty line
|
||||||
|
func indentLines(text, indent string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
var indentedLines []string
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
indentedLines = append(indentedLines, "")
|
||||||
|
} else {
|
||||||
|
indentedLines = append(indentedLines, indent+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(indentedLines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatLineByLine attempts to format each statement individually
|
||||||
|
func formatLineByLine(src string, fset *token.FileSet) (string, error) {
|
||||||
|
lines := strings.Split(src, "\n")
|
||||||
|
var formattedLines []string
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmedLine := strings.TrimSpace(line)
|
||||||
|
if trimmedLine == "" {
|
||||||
|
formattedLines = append(formattedLines, "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different wrapping strategies for individual lines
|
||||||
|
attempts := []string{
|
||||||
|
fmt.Sprintf("package main\n\nfunc main() {\n\t%s\n}", trimmedLine),
|
||||||
|
fmt.Sprintf("package main\n\n%s", trimmedLine),
|
||||||
|
fmt.Sprintf("package main\n\nvar _ = %s", trimmedLine),
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := trimmedLine // fallback
|
||||||
|
for _, attempt := range attempts {
|
||||||
|
if parsed, err := parser.ParseFile(fset, "", attempt, parser.ParseComments); err == nil {
|
||||||
|
if result, err := formatASTNode(fset, parsed); err == nil {
|
||||||
|
if extracted := extractSingleStatement(result, trimmedLine); extracted != "" {
|
||||||
|
formatted = extracted
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedLines = append(formattedLines, formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(formattedLines, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSingleStatement tries to extract a single formatted statement
|
||||||
|
func extractSingleStatement(formatted, original string) string {
|
||||||
|
lines := strings.Split(formatted, "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed != "" && !strings.HasPrefix(trimmed, "package ") &&
|
||||||
|
!strings.HasPrefix(trimmed, "func ") && !strings.HasPrefix(trimmed, "var _ =") &&
|
||||||
|
trimmed != "{" && trimmed != "}" {
|
||||||
|
// Remove leading tabs/spaces to normalize indentation
|
||||||
|
return strings.TrimLeft(line, " \t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatGo is a JavaScript-callable function that formats Go source code.
|
||||||
|
// It attempts multiple strategies to format code, including handling incomplete
|
||||||
|
// or syntactically invalid code fragments.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - this: The JavaScript 'this' context (unused)
|
||||||
|
// - i: JavaScript arguments array where i[0] should contain the Go source code as a string
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - js.Value: The formatted Go source code as a JavaScript string value
|
||||||
|
// - If formatting fails completely, returns the original code unchanged
|
||||||
|
// - If no arguments are provided, returns js.Null() and logs an error
|
||||||
|
func formatGo(this js.Value, i []js.Value) interface{} {
|
||||||
|
if len(i) == 0 {
|
||||||
|
js.Global().Get("console").Call("error", "formatGo: missing code argument")
|
||||||
|
return js.Null()
|
||||||
|
}
|
||||||
|
|
||||||
|
code := i[0].String()
|
||||||
|
if strings.TrimSpace(code) == "" {
|
||||||
|
return js.ValueOf(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, err := formatGoCode(code)
|
||||||
|
if err != nil {
|
||||||
|
js.Global().Get("console").Call("warn", "Go formatting had issues:", err.Error())
|
||||||
|
return js.ValueOf(code) // Return original code if all attempts fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return js.ValueOf(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// main initializes the WebAssembly module and exposes the formatGo function
|
||||||
|
// to the JavaScript global scope.
|
||||||
|
func main() {
|
||||||
|
// Create a channel to keep the Go program running
|
||||||
|
c := make(chan struct{}, 0)
|
||||||
|
|
||||||
|
// Expose the formatGo function to the JavaScript global scope
|
||||||
|
js.Global().Set("formatGo", js.FuncOf(formatGo))
|
||||||
|
|
||||||
|
// Block forever
|
||||||
|
<-c
|
||||||
|
}
|
||||||
4
frontend/src/utils/prettier/plugins/php/index.d.ts
vendored
Normal file
4
frontend/src/utils/prettier/plugins/php/index.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { Plugin } from "prettier";
|
||||||
|
|
||||||
|
declare const plugin: Plugin;
|
||||||
|
export default plugin;
|
||||||
19
frontend/src/utils/prettier/plugins/php/index.mjs
Normal file
19
frontend/src/utils/prettier/plugins/php/index.mjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export {
|
||||||
|
languages,
|
||||||
|
printers,
|
||||||
|
parsers,
|
||||||
|
options,
|
||||||
|
defaultOptions
|
||||||
|
} from './src/index.mjs';
|
||||||
|
|
||||||
|
import { languages, printers, parsers, options, defaultOptions } from './src/index.mjs';
|
||||||
|
|
||||||
|
const phpPlugin = {
|
||||||
|
languages,
|
||||||
|
printers,
|
||||||
|
parsers,
|
||||||
|
options,
|
||||||
|
defaultOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
export default phpPlugin;
|
||||||
111
frontend/src/utils/prettier/plugins/php/src/clean.mjs
Normal file
111
frontend/src/utils/prettier/plugins/php/src/clean.mjs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { printNumber, normalizeMagicMethodName } from "./util.mjs";
|
||||||
|
|
||||||
|
const ignoredProperties = new Set([
|
||||||
|
"loc",
|
||||||
|
"range",
|
||||||
|
"raw",
|
||||||
|
"comments",
|
||||||
|
"leadingComments",
|
||||||
|
"trailingComments",
|
||||||
|
"parenthesizedExpression",
|
||||||
|
"parent",
|
||||||
|
"prev",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
"tokens",
|
||||||
|
"errors",
|
||||||
|
"extra",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function takes the existing ast node and a copy, by reference
|
||||||
|
* We use it for testing, so that we can compare pre-post versions of the AST,
|
||||||
|
* excluding things we don't care about (like node location, case that will be
|
||||||
|
* changed by the printer, etc.)
|
||||||
|
*/
|
||||||
|
function clean(node, newObj) {
|
||||||
|
if (node.kind === "string") {
|
||||||
|
// TODO if options are available in this method, replace with
|
||||||
|
// newObj.isDoubleQuote = !useSingleQuote(node, options);
|
||||||
|
delete newObj.isDoubleQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["array", "list"].includes(node.kind)) {
|
||||||
|
// TODO if options are available in this method, assign instead of delete
|
||||||
|
delete newObj.shortForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "inline") {
|
||||||
|
if (node.value.includes("___PSEUDO_INLINE_PLACEHOLDER___")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj.value = newObj.value.replace(/\n/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue ((2)); -> continue 2;
|
||||||
|
// continue 1; -> continue;
|
||||||
|
if ((node.kind === "continue" || node.kind === "break") && node.level) {
|
||||||
|
const { level } = newObj;
|
||||||
|
|
||||||
|
if (level.kind === "number") {
|
||||||
|
newObj.level = level.value === "1" ? null : level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if () {{ }} -> if () {}
|
||||||
|
if (node.kind === "block") {
|
||||||
|
if (node.children.length === 1 && node.children[0].kind === "block") {
|
||||||
|
while (newObj.children[0].kind === "block") {
|
||||||
|
newObj.children = newObj.children[0].children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize numbers
|
||||||
|
if (node.kind === "number") {
|
||||||
|
newObj.value = printNumber(node.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statements = ["foreach", "for", "if", "while", "do"];
|
||||||
|
|
||||||
|
if (statements.includes(node.kind)) {
|
||||||
|
if (node.body && node.body.kind !== "block") {
|
||||||
|
newObj.body = {
|
||||||
|
kind: "block",
|
||||||
|
children: [newObj.body],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newObj.body = newObj.body ? newObj.body : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.alternate && node.alternate.kind !== "block") {
|
||||||
|
newObj.alternate = {
|
||||||
|
kind: "block",
|
||||||
|
children: [newObj.alternate],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newObj.alternate = newObj.alternate ? newObj.alternate : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "usegroup" && typeof node.name === "string") {
|
||||||
|
newObj.name = newObj.name.replace(/^\\/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "useitem") {
|
||||||
|
newObj.name = newObj.name.replace(/^\\/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "method" && node.name.kind === "identifier") {
|
||||||
|
newObj.name.name = normalizeMagicMethodName(newObj.name.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "noop") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clean.ignoredProperties = ignoredProperties;
|
||||||
|
|
||||||
|
export default clean;
|
||||||
1044
frontend/src/utils/prettier/plugins/php/src/comments.mjs
Normal file
1044
frontend/src/utils/prettier/plugins/php/src/comments.mjs
Normal file
File diff suppressed because it is too large
Load Diff
173
frontend/src/utils/prettier/plugins/php/src/index.mjs
Normal file
173
frontend/src/utils/prettier/plugins/php/src/index.mjs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { doc } from "prettier";
|
||||||
|
import LINGUIST_LANGUAGES_PHP from "linguist-languages/data/PHP";
|
||||||
|
import LINGUIST_LANGUAGES_HTML_PHP from "linguist-languages/data/HTML_2b_PHP";
|
||||||
|
import parse from "./parser.mjs";
|
||||||
|
import print from "./printer.mjs";
|
||||||
|
import clean from "./clean.mjs";
|
||||||
|
import options from "./options.mjs";
|
||||||
|
import {
|
||||||
|
handleOwnLineComment,
|
||||||
|
handleEndOfLineComment,
|
||||||
|
handleRemainingComment,
|
||||||
|
getCommentChildNodes,
|
||||||
|
canAttachComment,
|
||||||
|
isBlockComment,
|
||||||
|
} from "./comments.mjs";
|
||||||
|
import { hasPragma, insertPragma } from "./pragma.mjs";
|
||||||
|
import { locStart, locEnd } from "./loc.mjs";
|
||||||
|
|
||||||
|
const { join, hardline } = doc.builders;
|
||||||
|
|
||||||
|
function createLanguage(linguistData, { extend, override }) {
|
||||||
|
const language = {};
|
||||||
|
|
||||||
|
for (const key in linguistData) {
|
||||||
|
const newKey = key === "languageId" ? "linguistLanguageId" : key;
|
||||||
|
language[newKey] = linguistData[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extend) {
|
||||||
|
for (const key in extend) {
|
||||||
|
language[key] = (language[key] || []).concat(extend[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in override) {
|
||||||
|
language[key] = override[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
createLanguage(LINGUIST_LANGUAGES_PHP, {
|
||||||
|
override: {
|
||||||
|
parsers: ["php"],
|
||||||
|
vscodeLanguageIds: ["php"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createLanguage(LINGUIST_LANGUAGES_HTML_PHP, {
|
||||||
|
override: {
|
||||||
|
parsers: ["php"],
|
||||||
|
vscodeLanguageIds: ["php"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const parsers = {
|
||||||
|
php: {
|
||||||
|
parse,
|
||||||
|
astFormat: "php",
|
||||||
|
locStart,
|
||||||
|
locEnd,
|
||||||
|
hasPragma,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ignoredKeys = new Set([
|
||||||
|
"kind",
|
||||||
|
"loc",
|
||||||
|
"errors",
|
||||||
|
"extra",
|
||||||
|
"comments",
|
||||||
|
"leadingComments",
|
||||||
|
"enclosingNode",
|
||||||
|
"precedingNode",
|
||||||
|
"followingNode",
|
||||||
|
]);
|
||||||
|
function getVisitorKeys(node, nonTraversableKeys) {
|
||||||
|
return Object.keys(node).filter(
|
||||||
|
(key) => !nonTraversableKeys.has(key) && !ignoredKeys.has(key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const printers = {
|
||||||
|
php: {
|
||||||
|
print,
|
||||||
|
getVisitorKeys,
|
||||||
|
insertPragma,
|
||||||
|
massageAstNode: clean,
|
||||||
|
getCommentChildNodes,
|
||||||
|
canAttachComment,
|
||||||
|
isBlockComment,
|
||||||
|
handleComments: {
|
||||||
|
ownLine: handleOwnLineComment,
|
||||||
|
endOfLine: handleEndOfLineComment,
|
||||||
|
remaining: handleRemainingComment,
|
||||||
|
},
|
||||||
|
willPrintOwnComments(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
return node && node.kind === "noop";
|
||||||
|
},
|
||||||
|
printComment(path) {
|
||||||
|
const comment = path.node;
|
||||||
|
|
||||||
|
switch (comment.kind) {
|
||||||
|
case "commentblock": {
|
||||||
|
// for now, don't touch single line block comments
|
||||||
|
if (!comment.value.includes("\n")) {
|
||||||
|
return comment.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = comment.value.split("\n");
|
||||||
|
// if this is a block comment, handle indentation
|
||||||
|
if (
|
||||||
|
lines
|
||||||
|
.slice(1, lines.length - 1)
|
||||||
|
.every((line) => line.trim()[0] === "*")
|
||||||
|
) {
|
||||||
|
return join(
|
||||||
|
hardline,
|
||||||
|
lines.map(
|
||||||
|
(line, index) =>
|
||||||
|
(index > 0 ? " " : "") +
|
||||||
|
(index < lines.length - 1 ? line.trim() : line.trimLeft())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise we can't be sure about indentation, so just print as is
|
||||||
|
return comment.value;
|
||||||
|
}
|
||||||
|
case "commentline": {
|
||||||
|
return comment.value.trimRight();
|
||||||
|
}
|
||||||
|
/* c8 ignore next 2 */
|
||||||
|
default:
|
||||||
|
throw new Error(`Not a comment: ${JSON.stringify(comment)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasPrettierIgnore(path) {
|
||||||
|
const isSimpleIgnore = (comment) =>
|
||||||
|
comment.value.includes("prettier-ignore") &&
|
||||||
|
!comment.value.includes("prettier-ignore-start") &&
|
||||||
|
!comment.value.includes("prettier-ignore-end");
|
||||||
|
|
||||||
|
const { node, parent: parentNode } = path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(node &&
|
||||||
|
node.kind !== "classconstant" &&
|
||||||
|
node.comments &&
|
||||||
|
node.comments.length > 0 &&
|
||||||
|
node.comments.some(isSimpleIgnore)) ||
|
||||||
|
// For proper formatting, the classconstant ignore formatting should
|
||||||
|
// run on the "constant" child
|
||||||
|
(node &&
|
||||||
|
node.kind === "constant" &&
|
||||||
|
parentNode &&
|
||||||
|
parentNode.kind === "classconstant" &&
|
||||||
|
parentNode.comments &&
|
||||||
|
parentNode.comments.length > 0 &&
|
||||||
|
parentNode.comments.some(isSimpleIgnore))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
tabWidth: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { languages, printers, parsers, options, defaultOptions };
|
||||||
4
frontend/src/utils/prettier/plugins/php/src/loc.mjs
Normal file
4
frontend/src/utils/prettier/plugins/php/src/loc.mjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const loc = (prop) => (node) => node.loc?.[prop]?.offset;
|
||||||
|
|
||||||
|
export const locStart = loc("start");
|
||||||
|
export const locEnd = loc("end");
|
||||||
250
frontend/src/utils/prettier/plugins/php/src/needs-parens.mjs
Normal file
250
frontend/src/utils/prettier/plugins/php/src/needs-parens.mjs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import { getPrecedence, shouldFlatten, isBitwiseOperator } from "./util.mjs";
|
||||||
|
|
||||||
|
function needsParens(path, options) {
|
||||||
|
const { parent } = path;
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { key, node } = path;
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
// No need parens for top level children of this nodes
|
||||||
|
"program",
|
||||||
|
"expressionstatement",
|
||||||
|
"namespace",
|
||||||
|
"declare",
|
||||||
|
"block",
|
||||||
|
|
||||||
|
// No need parens
|
||||||
|
"include",
|
||||||
|
"print",
|
||||||
|
"return",
|
||||||
|
"echo",
|
||||||
|
].includes(parent.kind)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (node.kind) {
|
||||||
|
case "pre":
|
||||||
|
case "post":
|
||||||
|
if (parent.kind === "unary") {
|
||||||
|
return (
|
||||||
|
node.kind === "pre" &&
|
||||||
|
((node.type === "+" && parent.type === "+") ||
|
||||||
|
(node.type === "-" && parent.type === "-"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// else fallthrough
|
||||||
|
case "unary":
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "unary":
|
||||||
|
return (
|
||||||
|
node.type === parent.type &&
|
||||||
|
(node.type === "+" || node.type === "-")
|
||||||
|
);
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
case "call":
|
||||||
|
return key === "what";
|
||||||
|
case "bin":
|
||||||
|
return parent.type === "**" && key === "left";
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case "bin": {
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "assign":
|
||||||
|
case "retif":
|
||||||
|
return ["and", "xor", "or"].includes(node.type);
|
||||||
|
case "silent":
|
||||||
|
case "cast":
|
||||||
|
// TODO: bug https://github.com/glayzzle/php-parser/issues/172
|
||||||
|
return node.parenthesizedExpression;
|
||||||
|
case "pre":
|
||||||
|
case "post":
|
||||||
|
case "unary":
|
||||||
|
return true;
|
||||||
|
case "call":
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
return key === "what";
|
||||||
|
case "bin": {
|
||||||
|
const po = parent.type;
|
||||||
|
const pp = getPrecedence(po);
|
||||||
|
const no = node.type;
|
||||||
|
const np = getPrecedence(no);
|
||||||
|
|
||||||
|
if (pp > np) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (po === "||" && no === "&&") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pp === np && key === "right") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pp === np && !shouldFlatten(po, no)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pp < np && no === "%") {
|
||||||
|
return po === "+" || po === "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parenthesis when working with bitwise operators
|
||||||
|
// It's not stricly needed but helps with code understanding
|
||||||
|
if (isBitwiseOperator(po)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup": {
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "call":
|
||||||
|
return key === "what" && node.parenthesizedExpression;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "clone":
|
||||||
|
case "new": {
|
||||||
|
const requiresParens =
|
||||||
|
node.kind === "clone" ||
|
||||||
|
(node.kind === "new" && options.phpVersion < 8.4);
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
case "call":
|
||||||
|
return key === "what" && requiresParens;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "yield": {
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
case "call":
|
||||||
|
return key === "what";
|
||||||
|
|
||||||
|
case "retif":
|
||||||
|
return key === "test";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return !!(node.key || node.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "assign": {
|
||||||
|
if (
|
||||||
|
parent.kind === "for" &&
|
||||||
|
(parent.init.includes(node) || parent.increment.includes(node))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
} else if (parent.kind === "assign") {
|
||||||
|
return false;
|
||||||
|
} else if (parent.kind === "static") {
|
||||||
|
return false;
|
||||||
|
} else if (
|
||||||
|
["if", "do", "while", "foreach", "switch"].includes(parent.kind)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
} else if (parent.kind === "silent") {
|
||||||
|
return false;
|
||||||
|
} else if (parent.kind === "call") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case "retif":
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "cast":
|
||||||
|
return true;
|
||||||
|
case "unary":
|
||||||
|
case "bin":
|
||||||
|
case "retif":
|
||||||
|
if (key === "test" && !parent.trueExpr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
case "call":
|
||||||
|
return key === "what";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case "closure":
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "call":
|
||||||
|
return key === "what";
|
||||||
|
|
||||||
|
// https://github.com/prettier/plugin-php/issues/1675
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case "silence":
|
||||||
|
case "cast":
|
||||||
|
// TODO: bug https://github.com/glayzzle/php-parser/issues/172
|
||||||
|
return node.parenthesizedExpression;
|
||||||
|
// else fallthrough
|
||||||
|
case "string":
|
||||||
|
case "array":
|
||||||
|
switch (parent.kind) {
|
||||||
|
case "propertylookup":
|
||||||
|
case "nullsafepropertylookup":
|
||||||
|
case "staticlookup":
|
||||||
|
case "offsetlookup":
|
||||||
|
case "call":
|
||||||
|
if (
|
||||||
|
["string", "array"].includes(node.kind) &&
|
||||||
|
parent.kind === "offsetlookup"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key === "what";
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case "print":
|
||||||
|
case "include":
|
||||||
|
return parent.kind === "bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default needsParens;
|
||||||
69
frontend/src/utils/prettier/plugins/php/src/options.mjs
Normal file
69
frontend/src/utils/prettier/plugins/php/src/options.mjs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const CATEGORY_PHP = "PHP";
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const SUPPORTED_PHP_VERSIONS = [
|
||||||
|
5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6,
|
||||||
|
7.0, 7.1, 7.2, 7.3, 7.4,
|
||||||
|
8.0, 8.1, 8.2, 8.3, 8.4,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LATEST_SUPPORTED_PHP_VERSION = Math.max(...SUPPORTED_PHP_VERSIONS);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the PHP version to a number based on the provided options.
|
||||||
|
*/
|
||||||
|
export function resolvePhpVersion(options) {
|
||||||
|
if (!options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.phpVersion === "auto" || options.phpVersion === "composer") {
|
||||||
|
options.phpVersion = LATEST_SUPPORTED_PHP_VERSION;
|
||||||
|
} else {
|
||||||
|
options.phpVersion = parseFloat(options.phpVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
phpVersion: {
|
||||||
|
since: "0.13.0",
|
||||||
|
category: CATEGORY_PHP,
|
||||||
|
type: "choice",
|
||||||
|
default: "auto",
|
||||||
|
description: "Minimum target PHP version.",
|
||||||
|
choices: [
|
||||||
|
...SUPPORTED_PHP_VERSIONS.map((v) => ({ value: v.toFixed(1) })),
|
||||||
|
{
|
||||||
|
value: "auto",
|
||||||
|
description: `Use latest PHP Version (${LATEST_SUPPORTED_PHP_VERSION})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
trailingCommaPHP: {
|
||||||
|
since: "0.0.0",
|
||||||
|
category: CATEGORY_PHP,
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
description: "Print trailing commas wherever possible when multi-line.",
|
||||||
|
},
|
||||||
|
braceStyle: {
|
||||||
|
since: "0.10.0",
|
||||||
|
category: CATEGORY_PHP,
|
||||||
|
type: "choice",
|
||||||
|
default: "per-cs",
|
||||||
|
description:
|
||||||
|
"Print one space or newline for code blocks (classes and functions).",
|
||||||
|
choices: [
|
||||||
|
{ value: "psr-2", description: "(deprecated) Use per-cs" },
|
||||||
|
{ value: "per-cs", description: "Use the PER Coding Style brace style." },
|
||||||
|
{ value: "1tbs", description: "Use 1tbs brace style." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
singleQuote: {
|
||||||
|
since: "0.0.0",
|
||||||
|
category: CATEGORY_PHP,
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: "Use single quotes instead of double quotes.",
|
||||||
|
},
|
||||||
|
};
|
||||||
67
frontend/src/utils/prettier/plugins/php/src/parser.mjs
Normal file
67
frontend/src/utils/prettier/plugins/php/src/parser.mjs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import engine from "php-parser";
|
||||||
|
import { LATEST_SUPPORTED_PHP_VERSION } from "./options.mjs";
|
||||||
|
import { resolvePhpVersion } from "./options.mjs";
|
||||||
|
|
||||||
|
function parse(text, opts) {
|
||||||
|
const inMarkdown = opts && opts.parentParser === "markdown";
|
||||||
|
|
||||||
|
if (!text && inMarkdown) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
resolvePhpVersion(opts);
|
||||||
|
|
||||||
|
// Todo https://github.com/glayzzle/php-parser/issues/170
|
||||||
|
text = text.replace(/\?>\n<\?/g, "?>\n___PSEUDO_INLINE_PLACEHOLDER___<?");
|
||||||
|
|
||||||
|
// initialize a new parser instance
|
||||||
|
const parser = new engine({
|
||||||
|
parser: {
|
||||||
|
extractDoc: true,
|
||||||
|
version: `${LATEST_SUPPORTED_PHP_VERSION}`,
|
||||||
|
},
|
||||||
|
ast: {
|
||||||
|
withPositions: true,
|
||||||
|
withSource: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasOpenPHPTag = text.indexOf("<?php") !== -1;
|
||||||
|
const parseAsEval = inMarkdown && !hasOpenPHPTag;
|
||||||
|
|
||||||
|
let ast;
|
||||||
|
try {
|
||||||
|
ast = parseAsEval ? parser.parseEval(text) : parser.parseCode(text);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof SyntaxError && "lineNumber" in err) {
|
||||||
|
err.loc = {
|
||||||
|
start: {
|
||||||
|
line: err.lineNumber,
|
||||||
|
column: err.columnNumber,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
delete err.lineNumber;
|
||||||
|
delete err.columnNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.extra = {
|
||||||
|
parseAsEval,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/glayzzle/php-parser/issues/155
|
||||||
|
// currently inline comments include the line break at the end, we need to
|
||||||
|
// strip those out and update the end location for each comment manually
|
||||||
|
ast.comments.forEach((comment) => {
|
||||||
|
if (comment.value[comment.value.length - 1] === "\n") {
|
||||||
|
comment.value = comment.value.slice(0, -1);
|
||||||
|
comment.loc.end.offset = comment.loc.end.offset - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parse;
|
||||||
92
frontend/src/utils/prettier/plugins/php/src/pragma.mjs
Normal file
92
frontend/src/utils/prettier/plugins/php/src/pragma.mjs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { memoize } from "./util.mjs";
|
||||||
|
import parse from "./parser.mjs";
|
||||||
|
|
||||||
|
const reHasPragma = /@prettier|@format/;
|
||||||
|
|
||||||
|
const getPageLevelDocBlock = memoize((text) => {
|
||||||
|
const parsed = parse(text);
|
||||||
|
|
||||||
|
const [firstChild] = parsed.children;
|
||||||
|
const [firstDocBlock] = parsed.comments.filter(
|
||||||
|
(el) => el.kind === "commentblock"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
firstChild &&
|
||||||
|
firstDocBlock &&
|
||||||
|
firstDocBlock.loc.start.line < firstChild.loc.start.line
|
||||||
|
) {
|
||||||
|
return firstDocBlock;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function hasPragma(text) {
|
||||||
|
// fast path optimization - check if the pragma shows up in the file at all
|
||||||
|
if (!reHasPragma.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLevelDocBlock = getPageLevelDocBlock(text);
|
||||||
|
|
||||||
|
if (pageLevelDocBlock) {
|
||||||
|
const { value } = pageLevelDocBlock;
|
||||||
|
|
||||||
|
return reHasPragma.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectPragma(docblock) {
|
||||||
|
let lines = docblock.split("\n");
|
||||||
|
|
||||||
|
if (lines.length === 1) {
|
||||||
|
// normalize to multiline for simplicity
|
||||||
|
const [, line] = /\/*\*\*(.*)\*\//.exec(lines[0]);
|
||||||
|
|
||||||
|
lines = ["/**", ` * ${line.trim()}`, " */"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the first @pragma
|
||||||
|
// if there happens to be one on the opening line, just put it on the next line.
|
||||||
|
const pragmaIndex = lines.findIndex((line) => /@\S/.test(line)) || 1;
|
||||||
|
|
||||||
|
// not found => index == -1, which conveniently will splice 1 from the end.
|
||||||
|
lines.splice(pragmaIndex, 0, " * @format");
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertPragma(text) {
|
||||||
|
const pageLevelDocBlock = getPageLevelDocBlock(text);
|
||||||
|
|
||||||
|
if (pageLevelDocBlock) {
|
||||||
|
const {
|
||||||
|
start: { offset: startOffset },
|
||||||
|
end: { offset: endOffset },
|
||||||
|
} = pageLevelDocBlock.loc;
|
||||||
|
const before = text.substring(0, startOffset);
|
||||||
|
const after = text.substring(endOffset);
|
||||||
|
|
||||||
|
return `${before}${injectPragma(pageLevelDocBlock.value, text)}${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openTag = "<?php";
|
||||||
|
|
||||||
|
if (!text.startsWith(openTag)) {
|
||||||
|
// bail out
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitAt = openTag.length;
|
||||||
|
const phpTag = text.substring(0, splitAt);
|
||||||
|
const after = text.substring(splitAt);
|
||||||
|
|
||||||
|
return `${phpTag}
|
||||||
|
/**
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { hasPragma, insertPragma };
|
||||||
2899
frontend/src/utils/prettier/plugins/php/src/printer.mjs
Normal file
2899
frontend/src/utils/prettier/plugins/php/src/printer.mjs
Normal file
File diff suppressed because it is too large
Load Diff
743
frontend/src/utils/prettier/plugins/php/src/util.mjs
Normal file
743
frontend/src/utils/prettier/plugins/php/src/util.mjs
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
import { util as prettierUtil } from "prettier";
|
||||||
|
import { locStart } from "./loc.mjs";
|
||||||
|
|
||||||
|
const { hasNewline, skipEverythingButNewLine, skipNewline } = prettierUtil;
|
||||||
|
|
||||||
|
function printNumber(rawNumber) {
|
||||||
|
return (
|
||||||
|
rawNumber
|
||||||
|
.toLowerCase()
|
||||||
|
// Remove unnecessary plus and zeroes from scientific notation.
|
||||||
|
.replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(\d)/, "$1$2$3")
|
||||||
|
// Remove unnecessary scientific notation (1e0).
|
||||||
|
.replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1")
|
||||||
|
// Make sure numbers always start with a digit.
|
||||||
|
.replace(/^([+-])?\./, "$10.")
|
||||||
|
// Remove extraneous trailing decimal zeroes.
|
||||||
|
.replace(/(\.\d+?)0+(?=e|$)/, "$1")
|
||||||
|
// Remove unnecessary .e notation
|
||||||
|
.replace(/\.(?=e)/, "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://php.net/manual/en/language.operators.precedence.php
|
||||||
|
const PRECEDENCE = new Map(
|
||||||
|
[
|
||||||
|
["or"],
|
||||||
|
["xor"],
|
||||||
|
["and"],
|
||||||
|
[
|
||||||
|
"=",
|
||||||
|
"+=",
|
||||||
|
"-=",
|
||||||
|
"*=",
|
||||||
|
"**=",
|
||||||
|
"/=",
|
||||||
|
".=",
|
||||||
|
"%=",
|
||||||
|
"&=",
|
||||||
|
"|=",
|
||||||
|
"^=",
|
||||||
|
"<<=",
|
||||||
|
">>=",
|
||||||
|
],
|
||||||
|
["??"],
|
||||||
|
["||"],
|
||||||
|
["&&"],
|
||||||
|
["|"],
|
||||||
|
["^"],
|
||||||
|
["&"],
|
||||||
|
["==", "===", "!=", "!==", "<>", "<=>"],
|
||||||
|
["<", ">", "<=", ">="],
|
||||||
|
[">>", "<<"],
|
||||||
|
["+", "-", "."],
|
||||||
|
["*", "/", "%"],
|
||||||
|
["!"],
|
||||||
|
["instanceof"],
|
||||||
|
["++", "--", "~"],
|
||||||
|
["**"],
|
||||||
|
].flatMap((operators, index) =>
|
||||||
|
operators.map((operator) => [operator, index])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
function getPrecedence(operator) {
|
||||||
|
return PRECEDENCE.get(operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalityOperators = ["==", "!=", "===", "!==", "<>", "<=>"];
|
||||||
|
const multiplicativeOperators = ["*", "/", "%"];
|
||||||
|
const bitshiftOperators = [">>", "<<"];
|
||||||
|
|
||||||
|
function isBitwiseOperator(operator) {
|
||||||
|
return (
|
||||||
|
!!bitshiftOperators[operator] ||
|
||||||
|
operator === "|" ||
|
||||||
|
operator === "^" ||
|
||||||
|
operator === "&"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldFlatten(parentOp, nodeOp) {
|
||||||
|
if (getPrecedence(nodeOp) !== getPrecedence(parentOp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** is right-associative
|
||||||
|
// x ** y ** z --> x ** (y ** z)
|
||||||
|
if (parentOp === "**") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x == y == z --> (x == y) == z
|
||||||
|
if (
|
||||||
|
equalityOperators.includes(parentOp) &&
|
||||||
|
equalityOperators.includes(nodeOp)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x * y % z --> (x * y) % z
|
||||||
|
if (
|
||||||
|
(nodeOp === "%" && multiplicativeOperators.includes(parentOp)) ||
|
||||||
|
(parentOp === "%" && multiplicativeOperators.includes(nodeOp))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x * y / z --> (x * y) / z
|
||||||
|
// x / y * z --> (x / y) * z
|
||||||
|
if (
|
||||||
|
nodeOp !== parentOp &&
|
||||||
|
multiplicativeOperators.includes(nodeOp) &&
|
||||||
|
multiplicativeOperators.includes(parentOp)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x << y << z --> (x << y) << z
|
||||||
|
if (
|
||||||
|
bitshiftOperators.includes(parentOp) &&
|
||||||
|
bitshiftOperators.includes(nodeOp)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeHasStatement(node) {
|
||||||
|
return [
|
||||||
|
"block",
|
||||||
|
"program",
|
||||||
|
"namespace",
|
||||||
|
"class",
|
||||||
|
"enum",
|
||||||
|
"interface",
|
||||||
|
"trait",
|
||||||
|
"traituse",
|
||||||
|
"declare",
|
||||||
|
].includes(node.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBodyFirstChild({ body }) {
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (body.kind === "block") {
|
||||||
|
body = body.children;
|
||||||
|
}
|
||||||
|
return body[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeListProperty(node) {
|
||||||
|
const body = node.children || node.body || node.adaptations;
|
||||||
|
return Array.isArray(body) ? body : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLast(arr) {
|
||||||
|
if (arr.length > 0) {
|
||||||
|
return arr[arr.length - 1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPenultimate(arr) {
|
||||||
|
if (arr.length > 1) {
|
||||||
|
return arr[arr.length - 2];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFirstChildrenInlineNode(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (node.kind === "program") {
|
||||||
|
const children = getNodeListProperty(node);
|
||||||
|
|
||||||
|
if (!children || children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children[0].kind === "inline";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "switch") {
|
||||||
|
if (!node.body) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = getNodeListProperty(node.body);
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstCase] = children;
|
||||||
|
|
||||||
|
if (!firstCase.body) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCaseChildren = getNodeListProperty(firstCase.body);
|
||||||
|
|
||||||
|
if (firstCaseChildren.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstCaseChildren[0].kind === "inline";
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstChild = getBodyFirstChild(node);
|
||||||
|
|
||||||
|
if (!firstChild) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstChild.kind === "inline";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocNode(node) {
|
||||||
|
return (
|
||||||
|
node.kind === "nowdoc" ||
|
||||||
|
(node.kind === "encapsed" && node.type === "heredoc")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heredoc/Nowdoc nodes need a trailing linebreak if they
|
||||||
|
* appear as function arguments or array elements
|
||||||
|
*/
|
||||||
|
function docShouldHaveTrailingNewline(path, recurse = 0) {
|
||||||
|
const node = path.getNode(recurse);
|
||||||
|
const parent = path.getNode(recurse + 1);
|
||||||
|
const parentParent = path.getNode(recurse + 2);
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(parentParent &&
|
||||||
|
["call", "new", "echo"].includes(parentParent.kind) &&
|
||||||
|
!["call", "array"].includes(parent.kind)) ||
|
||||||
|
parent.kind === "parameter"
|
||||||
|
) {
|
||||||
|
const lastIndex = parentParent.arguments.length - 1;
|
||||||
|
const index = parentParent.arguments.indexOf(parent);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentParent && parentParent.kind === "for") {
|
||||||
|
const initIndex = parentParent.init.indexOf(parent);
|
||||||
|
|
||||||
|
if (initIndex !== -1) {
|
||||||
|
return initIndex !== parentParent.init.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testIndex = parentParent.test.indexOf(parent);
|
||||||
|
|
||||||
|
if (testIndex !== -1) {
|
||||||
|
return testIndex !== parentParent.test.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementIndex = parentParent.increment.indexOf(parent);
|
||||||
|
|
||||||
|
if (incrementIndex !== -1) {
|
||||||
|
return incrementIndex !== parentParent.increment.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "bin") {
|
||||||
|
return (
|
||||||
|
parent.left === node || docShouldHaveTrailingNewline(path, recurse + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "case" && parent.test === node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "staticvariable") {
|
||||||
|
const lastIndex = parentParent.variables.length - 1;
|
||||||
|
const index = parentParent.variables.indexOf(parent);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "entry") {
|
||||||
|
if (parent.key === node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = parentParent.items.length - 1;
|
||||||
|
const index = parentParent.items.indexOf(parent);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["call", "new"].includes(parent.kind)) {
|
||||||
|
const lastIndex = parent.arguments.length - 1;
|
||||||
|
const index = parent.arguments.indexOf(node);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "echo") {
|
||||||
|
const lastIndex = parent.expressions.length - 1;
|
||||||
|
const index = parent.expressions.indexOf(node);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "array") {
|
||||||
|
const lastIndex = parent.items.length - 1;
|
||||||
|
const index = parent.items.indexOf(node);
|
||||||
|
|
||||||
|
return index !== lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.kind === "retif") {
|
||||||
|
return docShouldHaveTrailingNewline(path, recurse + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineShouldEndWithSemicolon(path) {
|
||||||
|
const { node, parent: parentNode } = path;
|
||||||
|
if (!parentNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// for single line control structures written in a shortform (ie without a block),
|
||||||
|
// we need to make sure the single body node gets a semicolon
|
||||||
|
if (
|
||||||
|
["for", "foreach", "while", "do", "if", "switch"].includes(
|
||||||
|
parentNode.kind
|
||||||
|
) &&
|
||||||
|
node.kind !== "block" &&
|
||||||
|
node.kind !== "if" &&
|
||||||
|
(parentNode.body === node || parentNode.alternate === node)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!nodeHasStatement(parentNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.kind === "echo" && node.shortForm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.kind === "traituse") {
|
||||||
|
return !node.adaptations;
|
||||||
|
}
|
||||||
|
if (node.kind === "method" && node.isAbstract) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.kind === "method") {
|
||||||
|
const { parent } = path;
|
||||||
|
if (parent && parent.kind === "interface") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"expressionstatement",
|
||||||
|
"do",
|
||||||
|
"usegroup",
|
||||||
|
"classconstant",
|
||||||
|
"propertystatement",
|
||||||
|
"traitprecedence",
|
||||||
|
"traitalias",
|
||||||
|
"goto",
|
||||||
|
"constantstatement",
|
||||||
|
"enumcase",
|
||||||
|
"global",
|
||||||
|
"static",
|
||||||
|
"echo",
|
||||||
|
"unset",
|
||||||
|
"return",
|
||||||
|
"break",
|
||||||
|
"continue",
|
||||||
|
"throw",
|
||||||
|
].includes(node.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileShouldEndWithHardline(path) {
|
||||||
|
const { node } = path;
|
||||||
|
const isProgramNode = node.kind === "program";
|
||||||
|
const lastNode = node.children && getLast(node.children);
|
||||||
|
|
||||||
|
if (!isProgramNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastNode && ["halt", "inline"].includes(lastNode.kind)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastNode &&
|
||||||
|
(lastNode.kind === "declare" || lastNode.kind === "namespace")
|
||||||
|
) {
|
||||||
|
const lastNestedNode =
|
||||||
|
lastNode.children.length > 0 && getLast(lastNode.children);
|
||||||
|
|
||||||
|
if (lastNestedNode && ["halt", "inline"].includes(lastNestedNode.kind)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeStripLeadingSlashFromUse(name) {
|
||||||
|
const nameWithoutLeadingSlash = name.replace(/^\\/, "");
|
||||||
|
if (nameWithoutLeadingSlash.indexOf("\\") !== -1) {
|
||||||
|
return nameWithoutLeadingSlash;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDanglingComments(node) {
|
||||||
|
return (
|
||||||
|
node.comments &&
|
||||||
|
node.comments.some((comment) => !comment.leading && !comment.trailing)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLookupNode(node) {
|
||||||
|
return (
|
||||||
|
node.kind === "propertylookup" ||
|
||||||
|
node.kind === "nullsafepropertylookup" ||
|
||||||
|
node.kind === "staticlookup" ||
|
||||||
|
node.kind === "offsetlookup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrintHardLineAfterStartInControlStructure(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (["try", "catch"].includes(node.kind)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFirstChildrenInlineNode(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrintHardLineBeforeEndInControlStructure(path) {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
if (["try", "catch"].includes(node.kind)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === "switch") {
|
||||||
|
const children = getNodeListProperty(node.body);
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCase = getLast(children);
|
||||||
|
|
||||||
|
if (!lastCase.body) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childrenInCase = getNodeListProperty(lastCase.body);
|
||||||
|
|
||||||
|
if (childrenInCase.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return childrenInCase[0].kind !== "inline";
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isFirstChildrenInlineNode(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlignment(text) {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const lastLine = lines.pop();
|
||||||
|
|
||||||
|
return lastLine.length - lastLine.trimLeft().length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProgramLikeNode(node) {
|
||||||
|
return ["program", "declare", "namespace"].includes(node.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReferenceLikeNode(node) {
|
||||||
|
return [
|
||||||
|
"name",
|
||||||
|
"parentreference",
|
||||||
|
"selfreference",
|
||||||
|
"staticreference",
|
||||||
|
].includes(node.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `logical` value for `bin` node containing `||` or `&&` type otherwise return kind of node.
|
||||||
|
// Require for grouping logical and binary nodes in right way.
|
||||||
|
function getNodeKindIncludingLogical(node) {
|
||||||
|
if (node.kind === "bin" && ["||", "&&"].includes(node.type)) {
|
||||||
|
return "logical";
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if string can safely be converted from double to single quotes and vice-versa, i.e.
|
||||||
|
*
|
||||||
|
* - no embedded variables ("foo $bar")
|
||||||
|
* - no linebreaks
|
||||||
|
* - no special characters like \n, \t, ...
|
||||||
|
* - no octal/hex/unicode characters
|
||||||
|
*
|
||||||
|
* See https://php.net/manual/en/language.types.string.php#language.types.string.syntax.double
|
||||||
|
*/
|
||||||
|
function useDoubleQuote(node, options) {
|
||||||
|
if (node.isDoubleQuote === options.singleQuote) {
|
||||||
|
// We have a double quote and the user passed singleQuote:true, or the other way around.
|
||||||
|
const rawValue = node.raw.slice(node.raw[0] === "b" ? 2 : 1, -1);
|
||||||
|
const isComplex = rawValue.match(
|
||||||
|
/\\([$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u{([0-9a-fA-F]+)})|\r?\n|'|"|\$/
|
||||||
|
);
|
||||||
|
return node.isDoubleQuote ? isComplex : !isComplex;
|
||||||
|
}
|
||||||
|
return node.isDoubleQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEmptyBody(path, name = "body") {
|
||||||
|
const { node } = path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
node[name] &&
|
||||||
|
node[name].children &&
|
||||||
|
node[name].children.length === 0 &&
|
||||||
|
(!node[name].comments || node[name].comments.length === 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNextLineEmptyAfterNamespace(text, node) {
|
||||||
|
let idx = locStart(node);
|
||||||
|
idx = skipEverythingButNewLine(text, idx);
|
||||||
|
idx = skipNewline(text, idx);
|
||||||
|
return hasNewline(text, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPrintHardlineBeforeTrailingComma(lastElem) {
|
||||||
|
if (
|
||||||
|
lastElem.kind === "nowdoc" ||
|
||||||
|
(lastElem.kind === "encapsed" && lastElem.type === "heredoc")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastElem.kind === "entry" &&
|
||||||
|
(lastElem.value.kind === "nowdoc" ||
|
||||||
|
(lastElem.value.kind === "encapsed" && lastElem.value.type === "heredoc"))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestorCounter(path, typeOrTypes) {
|
||||||
|
const types = [].concat(typeOrTypes);
|
||||||
|
let counter = -1;
|
||||||
|
let ancestorNode;
|
||||||
|
while ((ancestorNode = path.getParentNode(++counter))) {
|
||||||
|
if (types.indexOf(ancestorNode.kind) !== -1) {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestorNode(path, typeOrTypes) {
|
||||||
|
const counter = getAncestorCounter(path, typeOrTypes);
|
||||||
|
return counter === -1 ? null : path.getParentNode(counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const magicMethods = [
|
||||||
|
"__construct",
|
||||||
|
"__destruct",
|
||||||
|
"__call",
|
||||||
|
"__callStatic",
|
||||||
|
"__get",
|
||||||
|
"__set",
|
||||||
|
"__isset",
|
||||||
|
"__unset",
|
||||||
|
"__sleep",
|
||||||
|
"__wakeup",
|
||||||
|
"__toString",
|
||||||
|
"__invoke",
|
||||||
|
"__set_state",
|
||||||
|
"__clone",
|
||||||
|
"__debugInfo",
|
||||||
|
];
|
||||||
|
const magicMethodsMap = new Map(
|
||||||
|
magicMethods.map((name) => [name.toLowerCase(), name])
|
||||||
|
);
|
||||||
|
|
||||||
|
function normalizeMagicMethodName(name) {
|
||||||
|
const loweredName = name.toLowerCase();
|
||||||
|
|
||||||
|
if (magicMethodsMap.has(loweredName)) {
|
||||||
|
return magicMethodsMap.get(loweredName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} kindsArray
|
||||||
|
* @returns {(node: Node | Comment) => Boolean}
|
||||||
|
*/
|
||||||
|
function createTypeCheckFunction(kindsArray) {
|
||||||
|
const kinds = new Set(kindsArray);
|
||||||
|
return (node) => kinds.has(node?.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSingleWordType = createTypeCheckFunction([
|
||||||
|
"variadicplaceholder",
|
||||||
|
"namedargument",
|
||||||
|
"nullkeyword",
|
||||||
|
"identifier",
|
||||||
|
"parameter",
|
||||||
|
"variable",
|
||||||
|
"variadic",
|
||||||
|
"boolean",
|
||||||
|
"literal",
|
||||||
|
"number",
|
||||||
|
"string",
|
||||||
|
"clone",
|
||||||
|
"cast",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isArrayExpression = createTypeCheckFunction(["array"]);
|
||||||
|
const isCallLikeExpression = createTypeCheckFunction([
|
||||||
|
"nullsafepropertylookup",
|
||||||
|
"propertylookup",
|
||||||
|
"staticlookup",
|
||||||
|
"offsetlookup",
|
||||||
|
"call",
|
||||||
|
"new",
|
||||||
|
]);
|
||||||
|
const isArrowFuncExpression = createTypeCheckFunction(["arrowfunc"]);
|
||||||
|
|
||||||
|
function getChainParts(node, prev = []) {
|
||||||
|
const parts = prev;
|
||||||
|
if (isCallLikeExpression(node)) {
|
||||||
|
parts.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node.what) {
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getChainParts(node.what, parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSimpleCallArgument(node, depth = 2) {
|
||||||
|
if (depth <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChildSimple = (child) => isSimpleCallArgument(child, depth - 1);
|
||||||
|
|
||||||
|
if (isSingleWordType(node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrayExpression(node)) {
|
||||||
|
return node.items.every((x) => x === null || isChildSimple(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCallLikeExpression(node)) {
|
||||||
|
const parts = getChainParts(node);
|
||||||
|
parts.unshift();
|
||||||
|
|
||||||
|
return (
|
||||||
|
parts.length <= depth &&
|
||||||
|
parts.every((node) =>
|
||||||
|
isLookupNode(node)
|
||||||
|
? isChildSimple(node.offset)
|
||||||
|
: node.arguments.every(isChildSimple)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrowFuncExpression(node)) {
|
||||||
|
return (
|
||||||
|
node.arguments.length <= depth && node.arguments.every(isChildSimple)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function memoize(fn) {
|
||||||
|
const cache = new Map();
|
||||||
|
return (key) => {
|
||||||
|
if (!cache.has(key)) {
|
||||||
|
cache.set(key, fn(key));
|
||||||
|
}
|
||||||
|
return cache.get(key);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
printNumber,
|
||||||
|
getPrecedence,
|
||||||
|
isBitwiseOperator,
|
||||||
|
shouldFlatten,
|
||||||
|
nodeHasStatement,
|
||||||
|
getLast,
|
||||||
|
getPenultimate,
|
||||||
|
getBodyFirstChild,
|
||||||
|
lineShouldEndWithSemicolon,
|
||||||
|
fileShouldEndWithHardline,
|
||||||
|
maybeStripLeadingSlashFromUse,
|
||||||
|
hasDanglingComments,
|
||||||
|
docShouldHaveTrailingNewline,
|
||||||
|
isLookupNode,
|
||||||
|
isFirstChildrenInlineNode,
|
||||||
|
shouldPrintHardLineAfterStartInControlStructure,
|
||||||
|
shouldPrintHardLineBeforeEndInControlStructure,
|
||||||
|
getAlignment,
|
||||||
|
isProgramLikeNode,
|
||||||
|
isReferenceLikeNode,
|
||||||
|
getNodeKindIncludingLogical,
|
||||||
|
useDoubleQuote,
|
||||||
|
hasEmptyBody,
|
||||||
|
isNextLineEmptyAfterNamespace,
|
||||||
|
shouldPrintHardlineBeforeTrailingComma,
|
||||||
|
isDocNode,
|
||||||
|
getAncestorNode,
|
||||||
|
normalizeMagicMethodName,
|
||||||
|
isSimpleCallArgument,
|
||||||
|
memoize,
|
||||||
|
};
|
||||||
1180
frontend/src/utils/prettier/plugins/sql/detect.mjs
Normal file
1180
frontend/src/utils/prettier/plugins/sql/detect.mjs
Normal file
File diff suppressed because it is too large
Load Diff
30
frontend/src/utils/prettier/plugins/sql/languages.js
Normal file
30
frontend/src/utils/prettier/plugins/sql/languages.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// SQL语言定义
|
||||||
|
export const languages = [
|
||||||
|
{
|
||||||
|
name: "SQL",
|
||||||
|
parsers: ["sql"],
|
||||||
|
extensions: [
|
||||||
|
".sql",
|
||||||
|
".ddl",
|
||||||
|
".dml",
|
||||||
|
".hql",
|
||||||
|
".psql",
|
||||||
|
".plsql",
|
||||||
|
".mysql",
|
||||||
|
".mssql",
|
||||||
|
".pgsql",
|
||||||
|
".sqlite",
|
||||||
|
".bigquery",
|
||||||
|
".snowflake",
|
||||||
|
".redshift",
|
||||||
|
".db2",
|
||||||
|
".n1ql",
|
||||||
|
".cql"
|
||||||
|
],
|
||||||
|
filenames: [
|
||||||
|
"*.sql",
|
||||||
|
"*.ddl",
|
||||||
|
"*.dml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
40
frontend/src/utils/prettier/plugins/sql/sql.d.ts
vendored
Normal file
40
frontend/src/utils/prettier/plugins/sql/sql.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { AST, Option } from 'node-sql-parser';
|
||||||
|
import { Options, ParserOptions, Plugin } from 'prettier';
|
||||||
|
import {
|
||||||
|
FormatOptions,
|
||||||
|
FormatOptionsWithLanguage,
|
||||||
|
} from 'sql-formatter';
|
||||||
|
|
||||||
|
export type SqlBaseOptions = Option &
|
||||||
|
Partial<
|
||||||
|
| (FormatOptions & { dialect: string })
|
||||||
|
| (FormatOptionsWithLanguage & { dialect?: never })
|
||||||
|
> & {
|
||||||
|
formatter?: 'sql-formatter' | 'node-sql-parser' | 'sql-cst';
|
||||||
|
params?: string;
|
||||||
|
paramTypes?: string;
|
||||||
|
autoDetectDialect?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SqlOptions = ParserOptions<AST> & SqlBaseOptions;
|
||||||
|
export type SqlFormatOptions = Options & SqlBaseOptions;
|
||||||
|
export declare const languages: Plugin["languages"];
|
||||||
|
|
||||||
|
export declare const parsers: {
|
||||||
|
sql: {
|
||||||
|
parse(text: string, options?: SqlOptions): AST | string;
|
||||||
|
astFormat: 'sql';
|
||||||
|
locStart(): number;
|
||||||
|
locEnd(): number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export declare const printers: Plugin["printers"];
|
||||||
|
|
||||||
|
export declare const options: Plugin["options"];
|
||||||
|
|
||||||
|
export declare function detectDialect(sql: string): string;
|
||||||
|
|
||||||
|
declare const SqlPlugin: Plugin<AST | string>;
|
||||||
|
|
||||||
|
export default SqlPlugin;
|
||||||
459
frontend/src/utils/prettier/plugins/sql/sql.mjs
Normal file
459
frontend/src/utils/prettier/plugins/sql/sql.mjs
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
import { JSOX } from 'jsox';
|
||||||
|
import nodeSqlParser from 'node-sql-parser';
|
||||||
|
import {
|
||||||
|
format,
|
||||||
|
formatDialect,
|
||||||
|
} from 'sql-formatter';
|
||||||
|
import { detectDialect } from './detect.mjs';
|
||||||
|
import { languages } from './languages.js';
|
||||||
|
|
||||||
|
const parser = new nodeSqlParser.Parser();
|
||||||
|
|
||||||
|
const SQL_FORMATTER = 'sql-formatter';
|
||||||
|
const NODE_SQL_PARSER = 'node-sql-parser';
|
||||||
|
|
||||||
|
// Parsers
|
||||||
|
export const parsers = {
|
||||||
|
sql: {
|
||||||
|
parse(text, options = {}) {
|
||||||
|
const { formatter, type, database } = options;
|
||||||
|
|
||||||
|
return formatter === SQL_FORMATTER
|
||||||
|
? text
|
||||||
|
: parser.astify(text, { type, database });
|
||||||
|
},
|
||||||
|
astFormat: 'sql',
|
||||||
|
locStart: () => -1,
|
||||||
|
locEnd: () => -1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Printers
|
||||||
|
export const printers = {
|
||||||
|
sql: {
|
||||||
|
print(path, options = {}) {
|
||||||
|
const value = path.node;
|
||||||
|
const {
|
||||||
|
formatter = SQL_FORMATTER,
|
||||||
|
type,
|
||||||
|
database,
|
||||||
|
dialect,
|
||||||
|
language,
|
||||||
|
params,
|
||||||
|
paramTypes,
|
||||||
|
autoDetectDialect = true,
|
||||||
|
...formatOptions
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let formatted;
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 准备sql-formatter选项
|
||||||
|
const sqlFormatterOptions = {
|
||||||
|
...formatOptions,
|
||||||
|
params:
|
||||||
|
params == null
|
||||||
|
? undefined
|
||||||
|
: JSOX.parse(params),
|
||||||
|
paramTypes:
|
||||||
|
paramTypes == null
|
||||||
|
? undefined
|
||||||
|
: JSOX.parse(paramTypes),
|
||||||
|
};
|
||||||
|
|
||||||
|
let finalLanguage = language;
|
||||||
|
let finalDialect = dialect;
|
||||||
|
|
||||||
|
if (autoDetectDialect && !language && !dialect) {
|
||||||
|
const detectedDialect = detectDialect(value);
|
||||||
|
finalLanguage = detectedDialect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用适当的格式化方法
|
||||||
|
if (finalDialect != null) {
|
||||||
|
// 使用formatDialect方法
|
||||||
|
formatted = formatDialect(value, {
|
||||||
|
...sqlFormatterOptions,
|
||||||
|
dialect: JSOX.parse(finalDialect),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 使用format方法
|
||||||
|
formatted = format(value, {
|
||||||
|
...sqlFormatterOptions,
|
||||||
|
language: finalLanguage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用node-sql-parser进行格式化
|
||||||
|
formatted = parser.sqlify(value, { type, database });
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted + '\n';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 插件选项
|
||||||
|
export const options = {
|
||||||
|
formatter: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'choice',
|
||||||
|
default: SQL_FORMATTER,
|
||||||
|
description: 'Choose which formatter to be used',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: SQL_FORMATTER,
|
||||||
|
description: 'use `sql-formatter` as formatter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: NODE_SQL_PARSER,
|
||||||
|
description: 'use `node-sql-parser` as formatter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
autoDetectDialect: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Automatically detect SQL dialect if language/dialect is not specified',
|
||||||
|
},
|
||||||
|
dialect: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'string',
|
||||||
|
description: 'SQL dialect for `sql-formatter` formatDialect()',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'sql',
|
||||||
|
description: 'SQL dialect for `sql-formatter` format()',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'sql',
|
||||||
|
description: 'Standard SQL: https://en.wikipedia.org/wiki/SQL:2011',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'bigquery',
|
||||||
|
description:
|
||||||
|
'Google Standard SQL (Bigquery): https://cloud.google.com/bigquery',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'db2',
|
||||||
|
description: 'IBM DB2: https://www.ibm.com/products/db2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'db2i',
|
||||||
|
description:
|
||||||
|
'IBM DB2i (experimental): https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'hive',
|
||||||
|
description: 'Apache Hive: https://hive.apache.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mariadb',
|
||||||
|
description: 'MariaDB: https://mariadb.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mysql',
|
||||||
|
description: 'MySQL: https://www.mysql.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'n1ql',
|
||||||
|
description:
|
||||||
|
'Couchbase N1QL: https://www.couchbase.com/products/n1ql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'plsql',
|
||||||
|
description:
|
||||||
|
'Oracle PL/SQL: https://www.oracle.com/database/technologies/appdev/plsql.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'postgresql',
|
||||||
|
description: 'PostgreSQL: https://www.postgresql.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'redshift',
|
||||||
|
description:
|
||||||
|
'Amazon Redshift: https://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'singlestoredb',
|
||||||
|
description:
|
||||||
|
'SingleStoreDB: https://docs.singlestore.com/db/v7.8/en/introduction/singlestore-documentation.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'snowflake',
|
||||||
|
description: 'Snowflake: https://docs.snowflake.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'spark',
|
||||||
|
description: 'Spark: https://spark.apache.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sqlite',
|
||||||
|
description: 'SQLite: https://www.sqlite.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'transactsql',
|
||||||
|
description:
|
||||||
|
'SQL Server Transact-SQL: https://docs.microsoft.com/en-us/sql/sql-server/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tsql',
|
||||||
|
description:
|
||||||
|
'SQL Server Transact-SQL: https://docs.microsoft.com/en-us/sql/sql-server/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trino',
|
||||||
|
description: 'Trino: https://trino.io',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
keywordCase: {
|
||||||
|
category: 'Output',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'preserve',
|
||||||
|
description:
|
||||||
|
'Converts reserved keywords to upper- or lowercase for `sql-formatter`',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'preserve',
|
||||||
|
description: 'preserves the original case of reserved keywords',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'upper',
|
||||||
|
description: 'converts reserved keywords to uppercase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lower',
|
||||||
|
description: 'converts reserved keywords to lowercase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
dataTypeCase: {
|
||||||
|
category: 'Output',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'preserve',
|
||||||
|
description:
|
||||||
|
'Converts data types to upper- or lowercase for `sql-formatter`',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'preserve',
|
||||||
|
description: 'preserves the original case of data types',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'upper',
|
||||||
|
description: 'converts data types to uppercase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lower',
|
||||||
|
description: 'converts data types to lowercase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
functionCase: {
|
||||||
|
category: 'Output',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'preserve',
|
||||||
|
description:
|
||||||
|
'Converts functions to upper- or lowercase for `sql-formatter`',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'preserve',
|
||||||
|
description: 'preserves the original case of functions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'upper',
|
||||||
|
description: 'converts functions to uppercase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lower',
|
||||||
|
description: 'converts functions to lowercase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
identifierCase: {
|
||||||
|
category: 'Output',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'preserve',
|
||||||
|
description:
|
||||||
|
'Converts identifiers to upper- or lowercase for `sql-formatter`. Only unquoted identifiers are converted. (experimental)',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'preserve',
|
||||||
|
description: 'preserves the original case of identifiers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'upper',
|
||||||
|
description: 'converts identifiers to uppercase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lower',
|
||||||
|
description: 'converts identifiers to lowercase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
uppercase: {
|
||||||
|
category: 'Output',
|
||||||
|
type: 'boolean',
|
||||||
|
deprecated: '0.7.0',
|
||||||
|
description: 'Use `keywordCase` option instead',
|
||||||
|
},
|
||||||
|
indentStyle: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'standard',
|
||||||
|
description: `Switches between different indentation styles for \`sql-formatter\`.
|
||||||
|
|
||||||
|
Caveats of using \`"tabularLeft"\` and \`"tabularRight"\`:
|
||||||
|
|
||||||
|
- \`tabWidth\` option is ignored. Indentation will always be 10 spaces, regardless of what is specified by \`tabWidth\``,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'standard',
|
||||||
|
description:
|
||||||
|
'indents code by the amount specified by `tabWidth` option',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tabularLeft',
|
||||||
|
description:
|
||||||
|
'indents in tabular style with 10 spaces, aligning keywords to left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tabularRight',
|
||||||
|
description:
|
||||||
|
'indents in tabular style with 10 spaces, aligning keywords to right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
logicalOperatorNewline: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'before',
|
||||||
|
description:
|
||||||
|
'Decides newline placement before or after logical operators (AND, OR, XOR)',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'before',
|
||||||
|
description: 'adds newline before the operator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'after',
|
||||||
|
description: 'adds newline after the operator',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
expressionWidth: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'int',
|
||||||
|
default: 50,
|
||||||
|
description:
|
||||||
|
'Determines maximum length of parenthesized expressions for `sql-formatter`',
|
||||||
|
},
|
||||||
|
linesBetweenQueries: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'int',
|
||||||
|
default: 1,
|
||||||
|
description:
|
||||||
|
'Decides how many empty lines to leave between SQL statements for `sql-formatter`',
|
||||||
|
},
|
||||||
|
denseOperators: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Decides whitespace around operators for `sql-formatter`. Does not apply to logical operators (AND, OR, XOR).',
|
||||||
|
},
|
||||||
|
newlineBeforeSemicolon: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to place query separator (`;`) on a separate line for `sql-formatter`',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
category: 'Format',
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Specifies `JSOX` **stringified** parameter values to fill in for placeholders inside SQL for `sql-formatter`. This option is designed to be used through API (though nothing really prevents usage from command line).',
|
||||||
|
},
|
||||||
|
paramTypes: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Specifies `JSOX` **stringified** parameter types to support when parsing SQL prepared statements for `sql-formatter`.',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'table',
|
||||||
|
description: 'Check the SQL with Authority List for `node-sql-parser`',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'table',
|
||||||
|
description: '`table` mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'column',
|
||||||
|
description: '`column` mode',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
category: 'Config',
|
||||||
|
type: 'choice',
|
||||||
|
default: 'mysql',
|
||||||
|
description: 'SQL dialect for `node-sql-parser`',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'bigquery',
|
||||||
|
description: 'BigQuery: https://cloud.google.com/bigquery',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'db2',
|
||||||
|
description: 'IBM DB2: https://www.ibm.com/analytics/db2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'hive',
|
||||||
|
description: 'Hive: https://hive.apache.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mariadb',
|
||||||
|
description: 'MariaDB: https://mariadb.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mysql',
|
||||||
|
description: 'MySQL: https://www.mysql.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'postgresql',
|
||||||
|
description: 'PostgreSQL: https://www.postgresql.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'transactsql',
|
||||||
|
description:
|
||||||
|
'TransactSQL: https://docs.microsoft.com/en-us/sql/t-sql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'flinksql',
|
||||||
|
description:
|
||||||
|
'FlinkSQL: https://ci.apache.org/projects/flink/flink-docs-stable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'snowflake',
|
||||||
|
description: 'Snowflake (alpha): https://docs.snowflake.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SqlPlugin = {
|
||||||
|
languages,
|
||||||
|
parsers,
|
||||||
|
printers,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { languages };
|
||||||
|
export default SqlPlugin;
|
||||||
@@ -20,14 +20,12 @@ import { StreamLanguage } from "@codemirror/language";
|
|||||||
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
|
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
|
||||||
import {shell} from "@codemirror/legacy-modes/mode/shell";
|
import {shell} from "@codemirror/legacy-modes/mode/shell";
|
||||||
import {go} from "@codemirror/legacy-modes/mode/go";
|
import {go} from "@codemirror/legacy-modes/mode/go";
|
||||||
import { csharp } from "@codemirror/legacy-modes/mode/clike";
|
import {csharp, kotlin, scala} from "@codemirror/legacy-modes/mode/clike";
|
||||||
import {clojure} from "@codemirror/legacy-modes/mode/clojure";
|
import {clojure} from "@codemirror/legacy-modes/mode/clojure";
|
||||||
import {erlang} from "@codemirror/legacy-modes/mode/erlang";
|
import {erlang} from "@codemirror/legacy-modes/mode/erlang";
|
||||||
import {swift} from "@codemirror/legacy-modes/mode/swift";
|
import {swift} from "@codemirror/legacy-modes/mode/swift";
|
||||||
import { kotlin } from "@codemirror/legacy-modes/mode/clike";
|
|
||||||
import {groovy} from "@codemirror/legacy-modes/mode/groovy";
|
import {groovy} from "@codemirror/legacy-modes/mode/groovy";
|
||||||
import {powerShell} from "@codemirror/legacy-modes/mode/powershell";
|
import {powerShell} from "@codemirror/legacy-modes/mode/powershell";
|
||||||
import { scala } from "@codemirror/legacy-modes/mode/clike";
|
|
||||||
import {toml} from "@codemirror/legacy-modes/mode/toml";
|
import {toml} from "@codemirror/legacy-modes/mode/toml";
|
||||||
import {elixir} from "codemirror-lang-elixir";
|
import {elixir} from "codemirror-lang-elixir";
|
||||||
import {SupportedLanguage} from '../types';
|
import {SupportedLanguage} from '../types';
|
||||||
@@ -38,6 +36,9 @@ import htmlPrettierPlugin from "prettier/plugins/html"
|
|||||||
import cssPrettierPlugin from "prettier/plugins/postcss"
|
import cssPrettierPlugin from "prettier/plugins/postcss"
|
||||||
import markdownPrettierPlugin from "prettier/plugins/markdown"
|
import markdownPrettierPlugin from "prettier/plugins/markdown"
|
||||||
import yamlPrettierPlugin from "prettier/plugins/yaml"
|
import yamlPrettierPlugin from "prettier/plugins/yaml"
|
||||||
|
import goPrettierPlugin from "@/utils/prettier/plugins/go/go"
|
||||||
|
import sqlPrettierPlugin from "@/utils/prettier/plugins/sql/sql"
|
||||||
|
import phpPrettierPlugin from "@/utils/prettier/plugins/php"
|
||||||
import * as prettierPluginEstree from "prettier/plugins/estree";
|
import * as prettierPluginEstree from "prettier/plugins/estree";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,8 +52,8 @@ export class LanguageInfo {
|
|||||||
public prettier?: {
|
public prettier?: {
|
||||||
parser: string;
|
parser: string;
|
||||||
plugins: any[];
|
plugins: any[];
|
||||||
|
}) {
|
||||||
}
|
}
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,13 +70,19 @@ export const LANGUAGES: LanguageInfo[] = [
|
|||||||
parser: "html",
|
parser: "html",
|
||||||
plugins: [htmlPrettierPlugin]
|
plugins: [htmlPrettierPlugin]
|
||||||
}),
|
}),
|
||||||
new LanguageInfo("sql", "SQL", StandardSQL.language.parser),
|
new LanguageInfo("sql", "SQL", StandardSQL.language.parser, {
|
||||||
|
parser: "sql",
|
||||||
|
plugins: [sqlPrettierPlugin]
|
||||||
|
}),
|
||||||
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
|
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
|
||||||
parser: "markdown",
|
parser: "markdown",
|
||||||
plugins: [markdownPrettierPlugin]
|
plugins: [markdownPrettierPlugin]
|
||||||
}),
|
}),
|
||||||
new LanguageInfo("java", "Java", javaLanguage.parser),
|
new LanguageInfo("java", "Java", javaLanguage.parser),
|
||||||
new LanguageInfo("php", "PHP", phpLanguage.configure({top:"Program"}).parser),
|
new LanguageInfo("php", "PHP", phpLanguage.configure({top: "Program"}).parser, {
|
||||||
|
parser: "php",
|
||||||
|
plugins: [phpPrettierPlugin]
|
||||||
|
}),
|
||||||
new LanguageInfo("css", "CSS", cssLanguage.parser, {
|
new LanguageInfo("css", "CSS", cssLanguage.parser, {
|
||||||
parser: "css",
|
parser: "css",
|
||||||
plugins: [cssPrettierPlugin]
|
plugins: [cssPrettierPlugin]
|
||||||
@@ -91,7 +98,10 @@ export const LANGUAGES: LanguageInfo[] = [
|
|||||||
plugins: [yamlPrettierPlugin]
|
plugins: [yamlPrettierPlugin]
|
||||||
}),
|
}),
|
||||||
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
|
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
|
||||||
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser),
|
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser, {
|
||||||
|
parser: "go",
|
||||||
|
plugins: [goPrettierPlugin]
|
||||||
|
}),
|
||||||
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
|
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
|
||||||
new LanguageInfo("ex", "Elixir", elixir().language.parser),
|
new LanguageInfo("ex", "Elixir", elixir().language.parser),
|
||||||
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
|
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const navItems = [
|
|||||||
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 仅在开发环境添加测试页面导航
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
navItems.push({ id: 'test', icon: '🧪', route: '/settings/test' });
|
||||||
|
}
|
||||||
|
|
||||||
const activeNavItem = ref(route.path.split('/').pop() || 'general');
|
const activeNavItem = ref(route.path.split('/').pop() || 'general');
|
||||||
|
|
||||||
// 处理导航点击
|
// 处理导航点击
|
||||||
@@ -56,7 +61,7 @@ const goBackToEditor = async () => {
|
|||||||
@click="handleNavClick(item)"
|
@click="handleNavClick(item)"
|
||||||
>
|
>
|
||||||
<span class="nav-icon">{{ item.icon }}</span>
|
<span class="nav-icon">{{ item.icon }}</span>
|
||||||
<span class="nav-text">{{ t(`settings.${item.id}`) }}</span>
|
<span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-footer">
|
<div class="settings-footer">
|
||||||
|
|||||||
274
frontend/src/views/settings/pages/TestPage.vue
Normal file
274
frontend/src/views/settings/pages/TestPage.vue
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-page">
|
||||||
|
<SettingSection title="Development Test Page">
|
||||||
|
<div class="dev-description">
|
||||||
|
This page is only available in development environment for testing notification and badge services.
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- Badge测试区域 -->
|
||||||
|
<SettingSection title="Badge Service Test">
|
||||||
|
<SettingItem title="Badge Text">
|
||||||
|
<input
|
||||||
|
v-model="badgeText"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter badge text (empty to remove)"
|
||||||
|
class="select-input"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title="Actions">
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="testBadge" class="test-button primary">
|
||||||
|
Set Badge
|
||||||
|
</button>
|
||||||
|
<button @click="clearBadge" class="test-button">
|
||||||
|
Clear Badge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
<div v-if="badgeStatus" class="test-status" :class="badgeStatus.type">
|
||||||
|
{{ badgeStatus.message }}
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 通知测试区域 -->
|
||||||
|
<SettingSection title="Notification Service Test">
|
||||||
|
<SettingItem title="Title">
|
||||||
|
<input
|
||||||
|
v-model="notificationTitle"
|
||||||
|
type="text"
|
||||||
|
placeholder="Notification title"
|
||||||
|
class="select-input"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title="Subtitle">
|
||||||
|
<input
|
||||||
|
v-model="notificationSubtitle"
|
||||||
|
type="text"
|
||||||
|
placeholder="Notification subtitle"
|
||||||
|
class="select-input"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title="Body">
|
||||||
|
<textarea
|
||||||
|
v-model="notificationBody"
|
||||||
|
placeholder="Notification body text"
|
||||||
|
class="select-input textarea-input"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title="Actions">
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="testNotification" class="test-button primary">
|
||||||
|
Send Test Notification
|
||||||
|
</button>
|
||||||
|
<button @click="testUpdateNotification" class="test-button">
|
||||||
|
Test Update Notification
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
<div v-if="notificationStatus" class="test-status" :class="notificationStatus.type">
|
||||||
|
{{ notificationStatus.message }}
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 清除所有测试状态 -->
|
||||||
|
<SettingSection title="Cleanup">
|
||||||
|
<SettingItem title="Clear All">
|
||||||
|
<button @click="clearAll" class="test-button danger">
|
||||||
|
Clear All Test States
|
||||||
|
</button>
|
||||||
|
</SettingItem>
|
||||||
|
<div v-if="clearStatus" class="test-status" :class="clearStatus.type">
|
||||||
|
{{ clearStatus.message }}
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
|
||||||
|
import SettingSection from '../components/SettingSection.vue'
|
||||||
|
import SettingItem from '../components/SettingItem.vue'
|
||||||
|
|
||||||
|
// Badge测试状态
|
||||||
|
const badgeText = ref('')
|
||||||
|
const badgeStatus = ref<{ type: string; message: string } | null>(null)
|
||||||
|
|
||||||
|
// 通知测试状态
|
||||||
|
const notificationTitle = ref('')
|
||||||
|
const notificationSubtitle = ref('')
|
||||||
|
const notificationBody = ref('')
|
||||||
|
const notificationStatus = ref<{ type: string; message: string } | null>(null)
|
||||||
|
|
||||||
|
// 清除状态
|
||||||
|
const clearStatus = ref<{ type: string; message: string } | null>(null)
|
||||||
|
|
||||||
|
// 显示状态消息的辅助函数
|
||||||
|
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
|
||||||
|
statusRef.value = { type, message }
|
||||||
|
setTimeout(() => {
|
||||||
|
statusRef.value = null
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试Badge功能
|
||||||
|
const testBadge = async () => {
|
||||||
|
try {
|
||||||
|
await TestService.TestBadge(badgeText.value)
|
||||||
|
showStatus(badgeStatus, 'success', `Badge ${badgeText.value ? 'set to: ' + badgeText.value : 'cleared'} successfully`)
|
||||||
|
} catch (error: any) {
|
||||||
|
showStatus(badgeStatus, 'error', `Failed to set badge: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除Badge
|
||||||
|
const clearBadge = async () => {
|
||||||
|
try {
|
||||||
|
await TestService.TestBadge('')
|
||||||
|
badgeText.value = ''
|
||||||
|
showStatus(badgeStatus, 'success', 'Badge cleared successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
showStatus(badgeStatus, 'error', `Failed to clear badge: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试通知功能
|
||||||
|
const testNotification = async () => {
|
||||||
|
try {
|
||||||
|
await TestService.TestNotification(
|
||||||
|
notificationTitle.value,
|
||||||
|
notificationSubtitle.value,
|
||||||
|
notificationBody.value
|
||||||
|
)
|
||||||
|
showStatus(notificationStatus, 'success', 'Notification sent successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
showStatus(notificationStatus, 'error', `Failed to send notification: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试更新通知
|
||||||
|
const testUpdateNotification = async () => {
|
||||||
|
try {
|
||||||
|
await TestService.TestUpdateNotification()
|
||||||
|
showStatus(notificationStatus, 'success', 'Update notification sent successfully (badge + notification)')
|
||||||
|
} catch (error: any) {
|
||||||
|
showStatus(notificationStatus, 'error', `Failed to send update notification: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有测试状态
|
||||||
|
const clearAll = async () => {
|
||||||
|
try {
|
||||||
|
await TestService.ClearAll()
|
||||||
|
// 清空表单
|
||||||
|
badgeText.value = ''
|
||||||
|
notificationTitle.value = ''
|
||||||
|
notificationSubtitle.value = ''
|
||||||
|
notificationBody.value = ''
|
||||||
|
showStatus(clearStatus, 'success', 'All test states cleared successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settings-page {
|
||||||
|
padding: 20px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-description {
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
color: var(--settings-text);
|
||||||
|
font-size: 12px;
|
||||||
|
width: 180px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.textarea-input {
|
||||||
|
min-height: 60px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--settings-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--settings-card-bg);
|
||||||
|
color: var(--settings-text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3a8eef;
|
||||||
|
border-color: #3a8eef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: var(--text-danger);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--text-danger);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-status {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: rgba(34, 197, 94, 0.1);
|
||||||
|
color: #16a34a;
|
||||||
|
border-color: rgba(34, 197, 94, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@ import {defineConfig, loadEnv} from 'vite';
|
|||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
export default defineConfig(({mode}: { mode: string }): object => {
|
export default defineConfig(({mode}: { mode: string }): object => {
|
||||||
const env: Record<string, string> = loadEnv(mode, process.cwd());
|
const env: Record<string, string> = loadEnv(mode, process.cwd());
|
||||||
@@ -15,6 +16,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
|
nodePolyfills(),
|
||||||
Components({
|
Components({
|
||||||
dts: true,
|
dts: true,
|
||||||
dirs: ['src/components'],
|
dirs: ['src/components'],
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -21,6 +21,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/42wim/httpsig v1.2.3 // indirect
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
@@ -77,6 +78,7 @@ require (
|
|||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
|
golang.org/x/image v0.24.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
|||||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
@@ -178,6 +180,8 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ func NewConfigMigrator(
|
|||||||
|
|
||||||
// AutoMigrate automatically detects and migrates missing configuration fields
|
// AutoMigrate automatically detects and migrates missing configuration fields
|
||||||
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
|
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
|
||||||
// Load default config into temporary koanf instance
|
|
||||||
defaultKoanf := koanf.New(".")
|
defaultKoanf := koanf.New(".")
|
||||||
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load default config: %w", err)
|
return nil, fmt.Errorf("failed to load default config: %w", err)
|
||||||
@@ -66,23 +65,20 @@ func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *
|
|||||||
result := &MigrationResult{
|
result := &MigrationResult{
|
||||||
MissingFields: missingFields,
|
MissingFields: missingFields,
|
||||||
Migrated: len(missingFields) > 0,
|
Migrated: len(missingFields) > 0,
|
||||||
Description: fmt.Sprintf("Detected %d missing configuration fields", len(missingFields)),
|
Description: fmt.Sprintf("Found %d missing fields", len(missingFields)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no missing fields, return early
|
// No migration needed
|
||||||
if !result.Migrated {
|
if !result.Migrated {
|
||||||
cm.logger.Info("No missing configuration fields detected")
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only create backup if we actually need to migrate (has missing fields)
|
// Create backup before migration
|
||||||
if len(missingFields) > 0 {
|
backupPath, err := cm.createBackup()
|
||||||
if backupPath, err := cm.createBackup(); err != nil {
|
if err != nil {
|
||||||
cm.logger.Error("Failed to create backup", "error", err)
|
return result, fmt.Errorf("backup creation failed: %w", err)
|
||||||
} else {
|
}
|
||||||
result.BackupPath = backupPath
|
result.BackupPath = backupPath
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge missing fields from default config
|
// Merge missing fields from default config
|
||||||
if err := cm.mergeDefaultFields(currentConfig, defaultKoanf, missingFields); err != nil {
|
if err := cm.mergeDefaultFields(currentConfig, defaultKoanf, missingFields); err != nil {
|
||||||
@@ -94,19 +90,25 @@ func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *
|
|||||||
return result, fmt.Errorf("failed to save updated config: %w", err)
|
return result, fmt.Errorf("failed to save updated config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.logger.Info("Configuration migration completed successfully", "migratedFields", len(missingFields))
|
// Clean up backup on success
|
||||||
|
if backupPath != "" {
|
||||||
|
if err := os.Remove(backupPath); err != nil {
|
||||||
|
cm.logger.Error("Failed to remove backup", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectMissingFields detects missing configuration fields
|
// detectMissingFields detects missing configuration fields
|
||||||
func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
|
func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
|
||||||
var missingFields []string
|
var missing []string
|
||||||
cm.findMissingFieldsRecursive("", defaultConfig, current, &missingFields)
|
cm.findMissing("", defaultConfig, current, &missing)
|
||||||
return missingFields
|
return missing
|
||||||
}
|
}
|
||||||
|
|
||||||
// findMissingFieldsRecursive recursively finds missing fields
|
// findMissing recursively finds missing fields
|
||||||
func (cm *ConfigMigrator) findMissingFieldsRecursive(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
|
func (cm *ConfigMigrator) findMissing(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
|
||||||
for key, defaultVal := range defaultMap {
|
for key, defaultVal := range defaultMap {
|
||||||
fullKey := key
|
fullKey := key
|
||||||
if prefix != "" {
|
if prefix != "" {
|
||||||
@@ -115,34 +117,38 @@ func (cm *ConfigMigrator) findMissingFieldsRecursive(prefix string, defaultMap,
|
|||||||
|
|
||||||
currentVal, exists := currentMap[key]
|
currentVal, exists := currentMap[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
// Field is completely missing
|
// Field completely missing - add it
|
||||||
*missing = append(*missing, fullKey)
|
*missing = append(*missing, fullKey)
|
||||||
} else {
|
} else if defaultNestedMap, ok := defaultVal.(map[string]interface{}); ok {
|
||||||
// Check nested structures
|
|
||||||
if defaultNestedMap, ok := defaultVal.(map[string]interface{}); ok {
|
|
||||||
if currentNestedMap, ok := currentVal.(map[string]interface{}); ok {
|
if currentNestedMap, ok := currentVal.(map[string]interface{}); ok {
|
||||||
cm.findMissingFieldsRecursive(fullKey, defaultNestedMap, currentNestedMap, missing)
|
// Both are maps, recurse into them
|
||||||
} else {
|
cm.findMissing(fullKey, defaultNestedMap, currentNestedMap, missing)
|
||||||
// Current value is not a map but default is, structure mismatch
|
|
||||||
*missing = append(*missing, fullKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Type mismatch: user has different type, don't recurse
|
||||||
}
|
}
|
||||||
|
// For non-map default values, field exists, preserve user's value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeDefaultFields merges default values for missing fields into current config
|
// mergeDefaultFields merges default values for missing fields into current config
|
||||||
func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf, missingFields []string) error {
|
func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf, missingFields []string) error {
|
||||||
|
actuallyMerged := 0
|
||||||
|
|
||||||
for _, field := range missingFields {
|
for _, field := range missingFields {
|
||||||
defaultValue := defaultConfig.Get(field)
|
if defaultConfig.Exists(field) {
|
||||||
if defaultValue != nil {
|
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
|
||||||
|
// Always set the field, even if it causes type conflicts
|
||||||
|
// This allows configuration structure evolution during upgrades
|
||||||
current.Set(field, defaultValue)
|
current.Set(field, defaultValue)
|
||||||
cm.logger.Debug("Merged missing field", "field", field, "value", defaultValue)
|
actuallyMerged++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last modified timestamp
|
// Update timestamp if we actually merged fields
|
||||||
|
if actuallyMerged > 0 {
|
||||||
current.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
current.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -165,7 +171,6 @@ func (cm *ConfigMigrator) createBackup() (string, error) {
|
|||||||
return "", fmt.Errorf("failed to create backup: %w", err)
|
return "", fmt.Errorf("failed to create backup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.logger.Info("Configuration backup created", "path", backupPath)
|
|
||||||
return backupPath, nil
|
return backupPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
@@ -158,10 +159,877 @@ func TestConfigMigrator_AutoMigrate(t *testing.T) {
|
|||||||
assert.True(t, k.Bool("newSection.enabled"), "newSection.enabled should be added with correct value")
|
assert.True(t, k.Bool("newSection.enabled"), "newSection.enabled should be added with correct value")
|
||||||
assert.Equal(t, "new section", k.String("newSection.value"), "newSection.value should be added with correct value")
|
assert.Equal(t, "new section", k.String("newSection.value"), "newSection.value should be added with correct value")
|
||||||
|
|
||||||
// Check that backup was created
|
// Check that backup was cleaned up after successful migration
|
||||||
backupFiles, err := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
backupFiles, err := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to list backup files: %v", err)
|
t.Fatalf("Failed to list backup files: %v", err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, 1, len(backupFiles), "One backup file should have been created")
|
assert.Equal(t, 0, len(backupFiles), "Backup file should have been cleaned up after successful migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_NoOverwrite tests that user configuration is never overwritten
|
||||||
|
func TestConfigMigrator_NoOverwrite(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_no_overwrite_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create user config with custom values that differ from defaults
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "CustomAppName", // Different from default
|
||||||
|
"version": "2.0.0", // Different from default
|
||||||
|
"theme": "custom", // Different from default
|
||||||
|
},
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"name": "Custom User", // Different from default
|
||||||
|
"email": "custom@example.com", // Different from default
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"autoSave": false, // Different from default
|
||||||
|
"language": "zh", // Different from default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config file
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
// Create migrator and load config
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create default config with different values
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "DefaultApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "light"
|
||||||
|
defaultConfig.User.Name = "Default User"
|
||||||
|
defaultConfig.User.Email = "default@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true // This should be added
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value" // This should be added
|
||||||
|
defaultConfig.NewSection.Enabled = true // This should be added
|
||||||
|
defaultConfig.NewSection.Value = "new section" // This should be added
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// Verify user values are preserved
|
||||||
|
assert.Equal(t, "CustomAppName", k.String("app.name"), "User's app name should not be overwritten")
|
||||||
|
assert.Equal(t, "2.0.0", k.String("app.version"), "User's version should not be overwritten")
|
||||||
|
assert.Equal(t, "custom", k.String("app.theme"), "User's theme should not be overwritten")
|
||||||
|
assert.Equal(t, "Custom User", k.String("user.name"), "User's name should not be overwritten")
|
||||||
|
assert.Equal(t, "custom@example.com", k.String("user.email"), "User's email should not be overwritten")
|
||||||
|
assert.False(t, k.Bool("user.settings.autoSave"), "User's autoSave should not be overwritten")
|
||||||
|
assert.Equal(t, "zh", k.String("user.settings.language"), "User's language should not be overwritten")
|
||||||
|
|
||||||
|
// Verify missing fields were added with default values
|
||||||
|
assert.True(t, k.Bool("user.settings.newSetting"), "Missing field should be added")
|
||||||
|
assert.Equal(t, "value", k.String("user.settings.newSetting2"), "Missing field should be added")
|
||||||
|
assert.True(t, k.Bool("newSection.enabled"), "Missing section should be added")
|
||||||
|
assert.Equal(t, "new section", k.String("newSection.value"), "Missing section should be added")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_TypeMismatch tests handling of type mismatches (config structure evolution)
|
||||||
|
func TestConfigMigrator_TypeMismatch(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_type_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create user config where some fields have different types
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "TestApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"theme": "dark",
|
||||||
|
},
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"settings": "simple_string", // This is a string, but default is an object
|
||||||
|
},
|
||||||
|
"newSection": 123, // This is a number, but default is an object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config file
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
// Create migrator and load config
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create default config
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "dark"
|
||||||
|
defaultConfig.User.Name = "Test User"
|
||||||
|
defaultConfig.User.Email = "test@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||||
|
defaultConfig.NewSection.Enabled = true
|
||||||
|
defaultConfig.NewSection.Value = "new section"
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Should detect missing fields and merge them, overriding type conflicts for config evolution
|
||||||
|
assert.True(t, result.Migrated, "Migration should be performed")
|
||||||
|
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields with type mismatch")
|
||||||
|
|
||||||
|
// Verify that type-mismatched values are overwritten with new structure (config evolution)
|
||||||
|
// This is important for software upgrades where config structure changes
|
||||||
|
assert.NotEqual(t, "simple_string", k.String("user.settings"), "User's string should be overwritten with new structure")
|
||||||
|
assert.NotEqual(t, int64(123), k.Int64("newSection"), "User's number should be overwritten with new structure")
|
||||||
|
|
||||||
|
// Verify new structure is properly applied
|
||||||
|
assert.True(t, k.Bool("user.settings.autoSave"), "New settings structure should be applied")
|
||||||
|
assert.Equal(t, "en", k.String("user.settings.language"), "New settings structure should be applied")
|
||||||
|
assert.True(t, k.Bool("user.settings.newSetting"), "New settings structure should be applied")
|
||||||
|
assert.Equal(t, "value", k.String("user.settings.newSetting2"), "New settings structure should be applied")
|
||||||
|
assert.True(t, k.Bool("newSection.enabled"), "New section structure should be applied")
|
||||||
|
assert.Equal(t, "new section", k.String("newSection.value"), "New section structure should be applied")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_ConfigEvolution tests configuration structure evolution scenarios
|
||||||
|
func TestConfigMigrator_ConfigEvolution(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_evolution_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Simulate old config format where "features" was a simple string
|
||||||
|
// but new version expects it to be an object
|
||||||
|
oldConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "MyApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"features": "plugin1,plugin2", // Old: simple comma-separated string
|
||||||
|
},
|
||||||
|
"database": "sqlite://data.db", // Old: simple string
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(oldConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// New config format where "features" and "database" are objects
|
||||||
|
type NewConfig struct {
|
||||||
|
App struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Features struct {
|
||||||
|
Enabled []string `json:"enabled"`
|
||||||
|
Config string `json:"config"`
|
||||||
|
} `json:"features"`
|
||||||
|
} `json:"app"`
|
||||||
|
Database struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig := NewConfig{}
|
||||||
|
defaultConfig.App.Name = "DefaultApp" // Will be preserved from user
|
||||||
|
defaultConfig.App.Version = "2.0.0" // Will be preserved from user
|
||||||
|
defaultConfig.App.Features.Enabled = []string{"newFeature"}
|
||||||
|
defaultConfig.App.Features.Config = "default.conf"
|
||||||
|
defaultConfig.Database.Type = "postgresql"
|
||||||
|
defaultConfig.Database.URL = "postgres://localhost:5432/db"
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// User's non-conflicting values should be preserved
|
||||||
|
assert.Equal(t, "MyApp", k.String("app.name"), "User's app name should be preserved")
|
||||||
|
assert.Equal(t, "1.0.0", k.String("app.version"), "User's version should be preserved")
|
||||||
|
|
||||||
|
// Conflicting structure should be evolved (old string → new object)
|
||||||
|
assert.NotEqual(t, "plugin1,plugin2", k.String("app.features"), "Old features string should be replaced")
|
||||||
|
assert.Equal(t, []string{"newFeature"}, k.Strings("app.features.enabled"), "New features structure should be applied")
|
||||||
|
assert.Equal(t, "default.conf", k.String("app.features.config"), "New features structure should be applied")
|
||||||
|
|
||||||
|
assert.NotEqual(t, "sqlite://data.db", k.String("database"), "Old database string should be replaced")
|
||||||
|
assert.Equal(t, "postgresql", k.String("database.type"), "New database structure should be applied")
|
||||||
|
assert.Equal(t, "postgres://localhost:5432/db", k.String("database.url"), "New database structure should be applied")
|
||||||
|
|
||||||
|
t.Logf("Successfully evolved config structure, fields migrated: %d", len(result.MissingFields))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_ComplexNested tests complex nested structure migration
|
||||||
|
func TestConfigMigrator_ComplexNested(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_complex_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Complex user config with deep nesting
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "TestApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"advanced": map[string]interface{}{
|
||||||
|
"logging": map[string]interface{}{
|
||||||
|
"level": "info",
|
||||||
|
"file": "/var/log/app.log",
|
||||||
|
// Missing: format, rotation
|
||||||
|
},
|
||||||
|
"performance": map[string]interface{}{
|
||||||
|
"cache": true,
|
||||||
|
// Missing: timeout, maxConnections
|
||||||
|
},
|
||||||
|
// Missing: security section
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"plugins": map[string]interface{}{
|
||||||
|
"enabled": []string{"plugin1", "plugin2"},
|
||||||
|
// Missing: config section
|
||||||
|
},
|
||||||
|
// Missing: monitoring section
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default config with additional nested fields
|
||||||
|
type ComplexConfig struct {
|
||||||
|
App struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Advanced struct {
|
||||||
|
Logging struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
File string `json:"file"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Rotation bool `json:"rotation"`
|
||||||
|
} `json:"logging"`
|
||||||
|
Performance struct {
|
||||||
|
Cache bool `json:"cache"`
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
MaxConnections int `json:"maxConnections"`
|
||||||
|
} `json:"performance"`
|
||||||
|
Security struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
TokenType string `json:"tokenType"`
|
||||||
|
ExpireTime int `json:"expireTime"`
|
||||||
|
} `json:"security"`
|
||||||
|
} `json:"advanced"`
|
||||||
|
} `json:"app"`
|
||||||
|
Plugins struct {
|
||||||
|
Enabled []string `json:"enabled"`
|
||||||
|
Config struct {
|
||||||
|
LoadOrder []string `json:"loadOrder"`
|
||||||
|
Settings map[string]string `json:"settings"`
|
||||||
|
} `json:"config"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
Monitoring struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
|
} `json:"monitoring"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config file
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
// Create migrator and load config
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create complete default config
|
||||||
|
defaultConfig := ComplexConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Advanced.Logging.Level = "info"
|
||||||
|
defaultConfig.App.Advanced.Logging.File = "/var/log/app.log"
|
||||||
|
defaultConfig.App.Advanced.Logging.Format = "json"
|
||||||
|
defaultConfig.App.Advanced.Logging.Rotation = true
|
||||||
|
defaultConfig.App.Advanced.Performance.Cache = true
|
||||||
|
defaultConfig.App.Advanced.Performance.Timeout = 30
|
||||||
|
defaultConfig.App.Advanced.Performance.MaxConnections = 100
|
||||||
|
defaultConfig.App.Advanced.Security.Enabled = true
|
||||||
|
defaultConfig.App.Advanced.Security.TokenType = "JWT"
|
||||||
|
defaultConfig.App.Advanced.Security.ExpireTime = 3600
|
||||||
|
defaultConfig.Plugins.Enabled = []string{"plugin1", "plugin2"}
|
||||||
|
defaultConfig.Plugins.Config.LoadOrder = []string{"plugin1", "plugin2"}
|
||||||
|
defaultConfig.Plugins.Config.Settings = map[string]string{"key": "value"}
|
||||||
|
defaultConfig.Monitoring.Enabled = true
|
||||||
|
defaultConfig.Monitoring.Endpoint = "/metrics"
|
||||||
|
defaultConfig.Monitoring.Interval = 60
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// Verify user values are preserved
|
||||||
|
assert.Equal(t, "info", k.String("app.advanced.logging.level"))
|
||||||
|
assert.Equal(t, "/var/log/app.log", k.String("app.advanced.logging.file"))
|
||||||
|
assert.True(t, k.Bool("app.advanced.performance.cache"))
|
||||||
|
|
||||||
|
// Verify missing fields were added
|
||||||
|
assert.Equal(t, "json", k.String("app.advanced.logging.format"))
|
||||||
|
assert.True(t, k.Bool("app.advanced.logging.rotation"))
|
||||||
|
assert.Equal(t, 30, k.Int("app.advanced.performance.timeout"))
|
||||||
|
assert.Equal(t, 100, k.Int("app.advanced.performance.maxConnections"))
|
||||||
|
assert.True(t, k.Bool("app.advanced.security.enabled"))
|
||||||
|
assert.Equal(t, "JWT", k.String("app.advanced.security.tokenType"))
|
||||||
|
assert.Equal(t, 3600, k.Int("app.advanced.security.expireTime"))
|
||||||
|
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.config.loadOrder"))
|
||||||
|
assert.True(t, k.Bool("monitoring.enabled"))
|
||||||
|
assert.Equal(t, "/metrics", k.String("monitoring.endpoint"))
|
||||||
|
assert.Equal(t, 60, k.Int("monitoring.interval"))
|
||||||
|
|
||||||
|
t.Logf("Detected missing fields: %v", result.MissingFields)
|
||||||
|
// Should detect multiple missing fields
|
||||||
|
assert.Greater(t, len(result.MissingFields), 5, "Should detect multiple missing fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_MultipleMigrations tests running migration multiple times
|
||||||
|
func TestConfigMigrator_MultipleMigrations(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_multiple_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create initial config
|
||||||
|
configPath := createTestConfig(t, tempDir)
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
|
||||||
|
// Create default config
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "dark"
|
||||||
|
defaultConfig.User.Name = "Test User"
|
||||||
|
defaultConfig.User.Email = "test@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||||
|
defaultConfig.NewSection.Enabled = true
|
||||||
|
defaultConfig.NewSection.Value = "new section"
|
||||||
|
|
||||||
|
// First migration
|
||||||
|
k1 := koanf.New(".")
|
||||||
|
k1.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
result1, err := migrator.AutoMigrate(defaultConfig, k1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result1.Migrated, "First migration should be performed")
|
||||||
|
|
||||||
|
// Second migration - should detect no missing fields
|
||||||
|
k2 := koanf.New(".")
|
||||||
|
k2.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
result2, err := migrator.AutoMigrate(defaultConfig, k2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, result2.Migrated, "Second migration should not be needed")
|
||||||
|
assert.Equal(t, 0, len(result2.MissingFields), "No fields should be missing in second migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_BackupHandling tests backup creation and cleanup
|
||||||
|
func TestConfigMigrator_BackupHandling(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_backup_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
configPath := createTestConfig(t, tempDir)
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
|
||||||
|
// Create default config
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "dark"
|
||||||
|
defaultConfig.User.Name = "Test User"
|
||||||
|
defaultConfig.User.Email = "test@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||||
|
defaultConfig.NewSection.Enabled = true
|
||||||
|
defaultConfig.NewSection.Value = "new section"
|
||||||
|
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// Backup should be cleaned up after successful migration
|
||||||
|
backupFiles, _ := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||||
|
assert.Equal(t, 0, len(backupFiles), "Backup should be cleaned up after successful migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_NoMigrationNeeded tests when no migration is needed
|
||||||
|
func TestConfigMigrator_NoMigrationNeeded(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_no_migration_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create complete config (no missing fields)
|
||||||
|
completeConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "TestApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"theme": "dark",
|
||||||
|
},
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"name": "Test User",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"autoSave": true,
|
||||||
|
"language": "en",
|
||||||
|
"newSetting": true,
|
||||||
|
"newSetting2": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"newSection": map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"value": "new section",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(completeConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create matching default config
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "dark"
|
||||||
|
defaultConfig.User.Name = "Test User"
|
||||||
|
defaultConfig.User.Email = "test@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||||
|
defaultConfig.NewSection.Enabled = true
|
||||||
|
defaultConfig.NewSection.Value = "new section"
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, result.Migrated, "No migration should be needed")
|
||||||
|
assert.Equal(t, 0, len(result.MissingFields), "No fields should be missing")
|
||||||
|
|
||||||
|
// No backup should be created
|
||||||
|
backupFiles, _ := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||||
|
assert.Equal(t, 0, len(backupFiles), "No backup should be created when migration is not needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_PartialOverride tests partial user override scenarios
|
||||||
|
func TestConfigMigrator_PartialOverride(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_partial_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Create user config with partial overrides
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"app": map[string]interface{}{
|
||||||
|
"name": "CustomApp",
|
||||||
|
"version": "2.0.0", // User custom value
|
||||||
|
// Missing: theme (should use default)
|
||||||
|
},
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"name": "Custom User",
|
||||||
|
"email": "custom@example.com",
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"autoSave": false, // User custom value
|
||||||
|
"language": "zh", // User custom value
|
||||||
|
// Missing: newSetting, newSetting2 (should use defaults)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Missing: newSection (should use defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create complete default config
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "DefaultApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "light" // Should be added
|
||||||
|
defaultConfig.User.Name = "Default User"
|
||||||
|
defaultConfig.User.Email = "default@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true // Should be added
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value" // Should be added
|
||||||
|
defaultConfig.NewSection.Enabled = true // Should be added
|
||||||
|
defaultConfig.NewSection.Value = "new section" // Should be added
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// Verify user values are preserved
|
||||||
|
assert.Equal(t, "CustomApp", k.String("app.name"))
|
||||||
|
assert.Equal(t, "2.0.0", k.String("app.version"))
|
||||||
|
assert.Equal(t, "Custom User", k.String("user.name"))
|
||||||
|
assert.Equal(t, "custom@example.com", k.String("user.email"))
|
||||||
|
assert.False(t, k.Bool("user.settings.autoSave"))
|
||||||
|
assert.Equal(t, "zh", k.String("user.settings.language"))
|
||||||
|
|
||||||
|
// Verify missing fields were added with defaults
|
||||||
|
assert.Equal(t, "light", k.String("app.theme"))
|
||||||
|
assert.True(t, k.Bool("user.settings.newSetting"))
|
||||||
|
assert.Equal(t, "value", k.String("user.settings.newSetting2"))
|
||||||
|
assert.True(t, k.Bool("newSection.enabled"))
|
||||||
|
assert.Equal(t, "new section", k.String("newSection.value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_ArrayMerge tests array and slice handling
|
||||||
|
func TestConfigMigrator_ArrayMerge(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_array_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Config with arrays
|
||||||
|
type ArrayConfig struct {
|
||||||
|
Plugins struct {
|
||||||
|
Enabled []string `json:"enabled"`
|
||||||
|
Config struct {
|
||||||
|
LoadOrder []string `json:"loadOrder"`
|
||||||
|
Settings map[string]string `json:"settings"`
|
||||||
|
} `json:"config"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
Database struct {
|
||||||
|
Hosts []string `json:"hosts"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
} `json:"database"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User config with some arrays
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"plugins": map[string]interface{}{
|
||||||
|
"enabled": []string{"plugin1", "plugin2"}, // User's plugin list
|
||||||
|
// Missing: config section
|
||||||
|
},
|
||||||
|
// Missing: database section
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create default config with arrays
|
||||||
|
defaultConfig := ArrayConfig{}
|
||||||
|
defaultConfig.Plugins.Enabled = []string{"defaultPlugin1", "defaultPlugin2"}
|
||||||
|
defaultConfig.Plugins.Config.LoadOrder = []string{"plugin1", "plugin2"}
|
||||||
|
defaultConfig.Plugins.Config.Settings = map[string]string{"timeout": "30"}
|
||||||
|
defaultConfig.Database.Hosts = []string{"localhost", "backup.host"}
|
||||||
|
defaultConfig.Database.Ports = []int{5432, 5433}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// User's array should be preserved
|
||||||
|
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.enabled"))
|
||||||
|
|
||||||
|
// Missing arrays should be added from defaults
|
||||||
|
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.config.loadOrder"))
|
||||||
|
assert.Equal(t, []string{"localhost", "backup.host"}, k.Strings("database.hosts"))
|
||||||
|
assert.Equal(t, []int{5432, 5433}, k.Ints("database.ports"))
|
||||||
|
|
||||||
|
expectedSettings := map[string]string{"timeout": "30"}
|
||||||
|
assert.Equal(t, expectedSettings, k.StringMap("plugins.config.settings"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_DeepNesting tests very deep nested structures
|
||||||
|
func TestConfigMigrator_DeepNesting(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_deep_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Deep nested config
|
||||||
|
type DeepConfig struct {
|
||||||
|
Level1 struct {
|
||||||
|
Level2 struct {
|
||||||
|
Level3 struct {
|
||||||
|
Level4 struct {
|
||||||
|
Level5 struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
} `json:"level5"`
|
||||||
|
} `json:"level4"`
|
||||||
|
} `json:"level3"`
|
||||||
|
} `json:"level2"`
|
||||||
|
} `json:"level1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User config with partial deep nesting
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"level1": map[string]interface{}{
|
||||||
|
"level2": map[string]interface{}{
|
||||||
|
"level3": map[string]interface{}{
|
||||||
|
// Missing level4 completely
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Create default config with complete deep nesting
|
||||||
|
defaultConfig := DeepConfig{}
|
||||||
|
defaultConfig.Level1.Level2.Level3.Level4.Level5.Value = "deep_value"
|
||||||
|
defaultConfig.Level1.Level2.Level3.Level4.Level5.Count = 42
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
|
||||||
|
// Verify deep nested values were added
|
||||||
|
assert.Equal(t, "deep_value", k.String("level1.level2.level3.level4.level5.value"))
|
||||||
|
assert.Equal(t, 42, k.Int("level1.level2.level3.level4.level5.count"))
|
||||||
|
|
||||||
|
t.Logf("Missing fields: %v", result.MissingFields)
|
||||||
|
assert.Equal(t, 2, len(result.MissingFields), "Should detect 2 missing deep nested fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_EdgeCases tests various edge cases
|
||||||
|
func TestConfigMigrator_EdgeCases(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_edge_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Edge case config with various data types
|
||||||
|
userConfig := map[string]interface{}{
|
||||||
|
"string_empty": "",
|
||||||
|
"string_spaces": " ",
|
||||||
|
"number_zero": 0,
|
||||||
|
"number_float": 3.14,
|
||||||
|
"bool_false": false,
|
||||||
|
"array_empty": []interface{}{},
|
||||||
|
"map_empty": map[string]interface{}{},
|
||||||
|
"null_value": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||||
|
os.WriteFile(configPath, jsonData, 0644)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
// Default config with different values
|
||||||
|
type EdgeConfig struct {
|
||||||
|
StringEmpty string `json:"string_empty"`
|
||||||
|
StringSpaces string `json:"string_spaces"`
|
||||||
|
NumberZero int `json:"number_zero"`
|
||||||
|
NumberFloat float64 `json:"number_float"`
|
||||||
|
BoolFalse bool `json:"bool_false"`
|
||||||
|
ArrayEmpty []string `json:"array_empty"`
|
||||||
|
MapEmpty map[string]interface{} `json:"map_empty"`
|
||||||
|
NullValue *string `json:"null_value"`
|
||||||
|
NewField string `json:"new_field"` // This should be added
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig := EdgeConfig{}
|
||||||
|
defaultConfig.StringEmpty = "default_string"
|
||||||
|
defaultConfig.StringSpaces = "default_spaces"
|
||||||
|
defaultConfig.NumberZero = 42
|
||||||
|
defaultConfig.NumberFloat = 2.71
|
||||||
|
defaultConfig.BoolFalse = true
|
||||||
|
defaultConfig.ArrayEmpty = []string{"default"}
|
||||||
|
defaultConfig.MapEmpty = map[string]interface{}{"key": "value"}
|
||||||
|
defaultValue := "default_null"
|
||||||
|
defaultConfig.NullValue = &defaultValue
|
||||||
|
defaultConfig.NewField = "new_field_value"
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// All user edge case values should be preserved (they exist, even if empty/zero/false)
|
||||||
|
assert.Equal(t, "", k.String("string_empty"))
|
||||||
|
assert.Equal(t, " ", k.String("string_spaces"))
|
||||||
|
assert.Equal(t, 0, k.Int("number_zero"))
|
||||||
|
assert.Equal(t, 3.14, k.Float64("number_float"))
|
||||||
|
assert.False(t, k.Bool("bool_false"))
|
||||||
|
assert.Equal(t, []string{}, k.Strings("array_empty"))
|
||||||
|
|
||||||
|
// Only truly missing field should be added
|
||||||
|
assert.Equal(t, "new_field_value", k.String("new_field"))
|
||||||
|
|
||||||
|
// Should detect 2 missing fields: new_field and map_empty.key
|
||||||
|
// The user has an empty map, but default config has a key inside that map
|
||||||
|
assert.Equal(t, 2, len(result.MissingFields), "Should detect 2 missing fields: new_field and map_empty.key")
|
||||||
|
assert.Contains(t, result.MissingFields, "new_field")
|
||||||
|
assert.Contains(t, result.MissingFields, "map_empty.key")
|
||||||
|
|
||||||
|
// Verify that the key was added to the empty map
|
||||||
|
assert.Equal(t, "value", k.String("map_empty.key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_ErrorHandling tests error conditions
|
||||||
|
func TestConfigMigrator_ErrorHandling(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_error_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
logger := log.New()
|
||||||
|
configPath := filepath.Join(tempDir, "nonexistent.json")
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||||
|
|
||||||
|
// Test with empty koanf (no config file loaded)
|
||||||
|
k := koanf.New(".")
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
|
||||||
|
// This should still work (creates config from scratch)
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, result.Migrated)
|
||||||
|
assert.Greater(t, len(result.MissingFields), 0)
|
||||||
|
|
||||||
|
// Test with corrupted config file
|
||||||
|
corruptedPath := filepath.Join(tempDir, "corrupted.json")
|
||||||
|
os.WriteFile(corruptedPath, []byte("{invalid json"), 0644)
|
||||||
|
|
||||||
|
k2 := koanf.New(".")
|
||||||
|
// This should fail gracefully when trying to load the corrupted file
|
||||||
|
err = k2.Load(file.Provider(corruptedPath), jsonparser.Parser())
|
||||||
|
assert.Error(t, err, "Should fail to load corrupted JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigMigrator_ConcurrentAccess tests concurrent migration access
|
||||||
|
func TestConfigMigrator_ConcurrentAccess(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "config_migrator_concurrent_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
configPath := createTestConfig(t, tempDir)
|
||||||
|
logger := log.New()
|
||||||
|
|
||||||
|
// Create multiple migrators
|
||||||
|
numWorkers := 5
|
||||||
|
results := make(chan *MigrationResult, numWorkers)
|
||||||
|
errors := make(chan error, numWorkers)
|
||||||
|
|
||||||
|
defaultConfig := TestConfig{}
|
||||||
|
defaultConfig.App.Name = "TestApp"
|
||||||
|
defaultConfig.App.Version = "1.0.0"
|
||||||
|
defaultConfig.App.Theme = "dark"
|
||||||
|
defaultConfig.User.Name = "Test User"
|
||||||
|
defaultConfig.User.Email = "test@example.com"
|
||||||
|
defaultConfig.User.Settings.AutoSave = true
|
||||||
|
defaultConfig.User.Settings.Language = "en"
|
||||||
|
defaultConfig.User.Settings.NewSetting = true
|
||||||
|
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||||
|
defaultConfig.NewSection.Enabled = true
|
||||||
|
defaultConfig.NewSection.Value = "new section"
|
||||||
|
|
||||||
|
// Run concurrent migrations
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
go func(workerID int) {
|
||||||
|
// Each worker gets its own config path to avoid file conflicts
|
||||||
|
workerConfigPath := filepath.Join(tempDir, fmt.Sprintf("config_%d.json", workerID))
|
||||||
|
|
||||||
|
// Copy the original config for this worker
|
||||||
|
originalData, _ := os.ReadFile(configPath)
|
||||||
|
os.WriteFile(workerConfigPath, originalData, 0644)
|
||||||
|
|
||||||
|
migrator := NewConfigMigrator(logger, tempDir, fmt.Sprintf("config_%d", workerID), workerConfigPath)
|
||||||
|
k := koanf.New(".")
|
||||||
|
k.Load(file.Provider(workerConfigPath), jsonparser.Parser())
|
||||||
|
|
||||||
|
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||||
|
if err != nil {
|
||||||
|
errors <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results <- result
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
for i := 0; i < numWorkers; i++ {
|
||||||
|
select {
|
||||||
|
case result := <-results:
|
||||||
|
assert.True(t, result.Migrated, "Each worker should successfully migrate")
|
||||||
|
assert.Equal(t, 4, len(result.MissingFields), "Each worker should detect same missing fields")
|
||||||
|
case err := <-errors:
|
||||||
|
t.Errorf("Worker failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
return cs.createDefaultConfig()
|
return cs.createDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查并自动迁移配置
|
|
||||||
cs.checkConfigMigration()
|
|
||||||
|
|
||||||
// 配置文件存在,直接加载现有配置
|
// 配置文件存在,直接加载现有配置
|
||||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||||
@@ -121,8 +118,8 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkConfigMigration 检查配置迁移
|
// MigrateConfig 执行配置迁移
|
||||||
func (cs *ConfigService) checkConfigMigration() error {
|
func (cs *ConfigService) MigrateConfig() error {
|
||||||
if cs.configMigrator == nil {
|
if cs.configMigrator == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package services
|
|||||||
#cgo CFLAGS: -I../lib
|
#cgo CFLAGS: -I../lib
|
||||||
#cgo LDFLAGS: -luser32
|
#cgo LDFLAGS: -luser32
|
||||||
#include "../lib/hotkey_windows.c"
|
#include "../lib/hotkey_windows.c"
|
||||||
#include "hotkey_windows.h"
|
#include "../lib/hotkey_windows.h"
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/creativeprojects/go-selfupdate"
|
"github.com/creativeprojects/go-selfupdate"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/badge"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,6 +30,8 @@ type SelfUpdateResult struct {
|
|||||||
type SelfUpdateService struct {
|
type SelfUpdateService struct {
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
badgeService *badge.BadgeService // 直接使用Wails原生badge服务
|
||||||
|
notificationService *notifications.NotificationService // 通知服务
|
||||||
config *models.AppConfig
|
config *models.AppConfig
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@@ -35,7 +39,7 @@ type SelfUpdateService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSelfUpdateService 创建自我更新服务实例
|
// NewSelfUpdateService 创建自我更新服务实例
|
||||||
func NewSelfUpdateService(configService *ConfigService, logger *log.LogService) (*SelfUpdateService, error) {
|
func NewSelfUpdateService(configService *ConfigService, badgeService *badge.BadgeService, notificationService *notifications.NotificationService, logger *log.LogService) (*SelfUpdateService, error) {
|
||||||
// 获取配置
|
// 获取配置
|
||||||
appConfig, err := configService.GetConfig()
|
appConfig, err := configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,6 +49,8 @@ func NewSelfUpdateService(configService *ConfigService, logger *log.LogService)
|
|||||||
service := &SelfUpdateService{
|
service := &SelfUpdateService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
badgeService: badgeService,
|
||||||
|
notificationService: notificationService,
|
||||||
config: appConfig,
|
config: appConfig,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
}
|
}
|
||||||
@@ -63,6 +69,7 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
|
|||||||
// 首先尝试主要更新源
|
// 首先尝试主要更新源
|
||||||
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
|
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
|
||||||
if err == nil && primaryResult != nil {
|
if err == nil && primaryResult != nil {
|
||||||
|
s.handleUpdateBadge(primaryResult)
|
||||||
return primaryResult, nil
|
return primaryResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +78,12 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
|
|||||||
if backupErr != nil {
|
if backupErr != nil {
|
||||||
// 如果备用源也失败,返回主要源的错误信息
|
// 如果备用源也失败,返回主要源的错误信息
|
||||||
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
|
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
|
||||||
|
// 确保在检查失败时也调用handleUpdateBadge来清除可能存在的badge
|
||||||
|
s.handleUpdateBadge(result)
|
||||||
return result, errors.New(result.Error)
|
return result, errors.New(result.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.handleUpdateBadge(backupResult)
|
||||||
return backupResult, nil
|
return backupResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +261,6 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
|
|||||||
|
|
||||||
// 检查是否有可用更新
|
// 检查是否有可用更新
|
||||||
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
|
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
|
||||||
s.logger.Info("Current version is up to date, no need to apply update")
|
|
||||||
result.LatestVersion = primaryRelease.Version()
|
result.LatestVersion = primaryRelease.Version()
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -310,6 +319,18 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
|
|||||||
s.logger.Error("Failed to update config version", "error", err)
|
s.logger.Error("Failed to update config version", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行配置迁移
|
||||||
|
if err := s.configService.MigrateConfig(); err != nil {
|
||||||
|
s.logger.Error("Failed to migrate config after update", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新成功,移除badge
|
||||||
|
if s.badgeService != nil {
|
||||||
|
if err := s.badgeService.RemoveBadge(); err != nil {
|
||||||
|
s.logger.Error("failed to remove update badge after successful update", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +386,6 @@ func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 尝试下载并应用更新,不设置超时
|
// 尝试下载并应用更新,不设置超时
|
||||||
s.logger.Info("Downloading update...", "source", sourceType)
|
|
||||||
err = updater.UpdateTo(ctx, release, exe)
|
err = updater.UpdateTo(ctx, release, exe)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -392,6 +412,13 @@ func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType mod
|
|||||||
s.logger.Error("Failed to update config version", "error", err)
|
s.logger.Error("Failed to update config version", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新成功,移除badge
|
||||||
|
if s.badgeService != nil {
|
||||||
|
if err := s.badgeService.RemoveBadge(); err != nil {
|
||||||
|
s.logger.Error("failed to remove update badge after successful update", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,3 +489,63 @@ func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleUpdateBadge 处理更新通知badge和通知
|
||||||
|
func (s *SelfUpdateService) handleUpdateBadge(result *SelfUpdateResult) {
|
||||||
|
if result != nil && result.HasUpdate {
|
||||||
|
// 有更新时显示更新badge
|
||||||
|
if s.badgeService != nil {
|
||||||
|
if err := s.badgeService.SetBadge("●"); err != nil {
|
||||||
|
s.logger.Error("failed to set update badge", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送简单通知
|
||||||
|
s.sendUpdateNotification(result)
|
||||||
|
} else {
|
||||||
|
// 没有更新或出错时移除badge
|
||||||
|
if s.badgeService != nil {
|
||||||
|
if err := s.badgeService.RemoveBadge(); err != nil {
|
||||||
|
s.logger.Error("failed to remove update badge", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendUpdateNotification 发送更新通知
|
||||||
|
func (s *SelfUpdateService) sendUpdateNotification(result *SelfUpdateResult) {
|
||||||
|
if s.notificationService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查通知授权(macOS需要)
|
||||||
|
authorized, err := s.notificationService.CheckNotificationAuthorization()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to check notification authorization", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authorized {
|
||||||
|
authorized, err = s.notificationService.RequestNotificationAuthorization()
|
||||||
|
if err != nil || !authorized {
|
||||||
|
s.logger.Error("Failed to get notification authorization", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建简单通知内容
|
||||||
|
title := "Voidraft Update Available"
|
||||||
|
body := fmt.Sprintf("New version %s available (current: %s)", result.LatestVersion, result.CurrentVersion)
|
||||||
|
|
||||||
|
// 发送简单通知
|
||||||
|
err = s.notificationService.SendNotification(notifications.NotificationOptions{
|
||||||
|
ID: "update_available",
|
||||||
|
Title: title,
|
||||||
|
Subtitle: "New version available",
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to send notification", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/badge"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceManager 服务管理器,负责协调各个服务
|
// ServiceManager 服务管理器,负责协调各个服务
|
||||||
@@ -25,6 +27,9 @@ type ServiceManager struct {
|
|||||||
selfUpdateService *SelfUpdateService
|
selfUpdateService *SelfUpdateService
|
||||||
translationService *TranslationService
|
translationService *TranslationService
|
||||||
themeService *ThemeService
|
themeService *ThemeService
|
||||||
|
badgeService *badge.BadgeService
|
||||||
|
notificationService *notifications.NotificationService
|
||||||
|
testService *TestService // 测试服务(仅开发环境)
|
||||||
BackupService *BackupService
|
BackupService *BackupService
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
}
|
}
|
||||||
@@ -34,6 +39,12 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化日志服务
|
// 初始化日志服务
|
||||||
logger := log.New()
|
logger := log.New()
|
||||||
|
|
||||||
|
// 初始化badge服务
|
||||||
|
badgeService := badge.New()
|
||||||
|
|
||||||
|
// 初始化通知服务
|
||||||
|
notificationService := notifications.New()
|
||||||
|
|
||||||
// 初始化配置服务
|
// 初始化配置服务
|
||||||
configService := NewConfigService(logger)
|
configService := NewConfigService(logger)
|
||||||
|
|
||||||
@@ -77,7 +88,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
startupService := NewStartupService(configService, logger)
|
startupService := NewStartupService(configService, logger)
|
||||||
|
|
||||||
// 初始化自我更新服务
|
// 初始化自我更新服务
|
||||||
selfUpdateService, err := NewSelfUpdateService(configService, logger)
|
selfUpdateService, err := NewSelfUpdateService(configService, badgeService, notificationService, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -91,6 +102,9 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化备份服务
|
// 初始化备份服务
|
||||||
backupService := NewBackupService(configService, databaseService, logger)
|
backupService := NewBackupService(configService, databaseService, logger)
|
||||||
|
|
||||||
|
// 初始化测试服务(开发环境使用)
|
||||||
|
testService := NewTestService(badgeService, notificationService, logger)
|
||||||
|
|
||||||
// 使用新的配置通知系统设置热键配置变更监听
|
// 使用新的配置通知系统设置热键配置变更监听
|
||||||
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||||
@@ -140,6 +154,9 @@ func NewServiceManager() *ServiceManager {
|
|||||||
selfUpdateService: selfUpdateService,
|
selfUpdateService: selfUpdateService,
|
||||||
translationService: translationService,
|
translationService: translationService,
|
||||||
themeService: themeService,
|
themeService: themeService,
|
||||||
|
badgeService: badgeService,
|
||||||
|
notificationService: notificationService,
|
||||||
|
testService: testService,
|
||||||
BackupService: backupService,
|
BackupService: backupService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
@@ -164,6 +181,9 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
|||||||
application.NewService(sm.selfUpdateService),
|
application.NewService(sm.selfUpdateService),
|
||||||
application.NewService(sm.translationService),
|
application.NewService(sm.translationService),
|
||||||
application.NewService(sm.themeService),
|
application.NewService(sm.themeService),
|
||||||
|
application.NewService(sm.badgeService),
|
||||||
|
application.NewService(sm.notificationService),
|
||||||
|
application.NewService(sm.testService), // 注册测试服务
|
||||||
application.NewService(sm.BackupService),
|
application.NewService(sm.BackupService),
|
||||||
}
|
}
|
||||||
return services
|
return services
|
||||||
@@ -243,3 +263,13 @@ func (sm *ServiceManager) GetThemeService() *ThemeService {
|
|||||||
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
|
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
|
||||||
return sm.windowSnapService
|
return sm.windowSnapService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBadgeService 获取badge服务实例
|
||||||
|
func (sm *ServiceManager) GetBadgeService() *badge.BadgeService {
|
||||||
|
return sm.badgeService
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationService 获取通知服务实例
|
||||||
|
func (sm *ServiceManager) GetNotificationService() *notifications.NotificationService {
|
||||||
|
return sm.notificationService
|
||||||
|
}
|
||||||
|
|||||||
139
internal/services/test_service.go
Normal file
139
internal/services/test_service.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/badge"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestService 测试服务 - 仅在开发环境使用
|
||||||
|
type TestService struct {
|
||||||
|
logger *log.LogService
|
||||||
|
badgeService *badge.BadgeService
|
||||||
|
notificationService *notifications.NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestService 创建测试服务实例
|
||||||
|
func NewTestService(badgeService *badge.BadgeService, notificationService *notifications.NotificationService, logger *log.LogService) *TestService {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TestService{
|
||||||
|
logger: logger,
|
||||||
|
badgeService: badgeService,
|
||||||
|
notificationService: notificationService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceStartup 服务启动时调用
|
||||||
|
func (ts *TestService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBadge 测试Badge功能
|
||||||
|
func (ts *TestService) TestBadge(text string) error {
|
||||||
|
if ts.badgeService == nil {
|
||||||
|
return fmt.Errorf("badge service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
|
// 如果文本为空,则移除badge
|
||||||
|
err := ts.badgeService.RemoveBadge()
|
||||||
|
if err != nil {
|
||||||
|
ts.logger.Error("Failed to remove badge", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ts.logger.Info("Badge removed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置badge
|
||||||
|
err := ts.badgeService.SetBadge(text)
|
||||||
|
if err != nil {
|
||||||
|
ts.logger.Error("Failed to set badge", "text", text, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.logger.Info("Badge set successfully", "text", text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNotification 测试通知功能
|
||||||
|
func (ts *TestService) TestNotification(title, subtitle, body string) error {
|
||||||
|
if ts.notificationService == nil {
|
||||||
|
return fmt.Errorf("notification service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查通知授权(macOS需要)
|
||||||
|
authorized, err := ts.notificationService.CheckNotificationAuthorization()
|
||||||
|
if err != nil {
|
||||||
|
ts.logger.Error("Failed to check notification authorization", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authorized {
|
||||||
|
authorized, err = ts.notificationService.RequestNotificationAuthorization()
|
||||||
|
if err != nil || !authorized {
|
||||||
|
ts.logger.Error("Failed to get notification authorization", "error", err)
|
||||||
|
return fmt.Errorf("notification authorization denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用默认值如果参数为空
|
||||||
|
if title == "" {
|
||||||
|
title = "Test Notification"
|
||||||
|
}
|
||||||
|
if subtitle == "" {
|
||||||
|
subtitle = "Testing notification system"
|
||||||
|
}
|
||||||
|
if body == "" {
|
||||||
|
body = "This is a test notification to verify the system is working correctly."
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送测试通知
|
||||||
|
err = ts.notificationService.SendNotification(notifications.NotificationOptions{
|
||||||
|
ID: "test_notification",
|
||||||
|
Title: title,
|
||||||
|
Subtitle: subtitle,
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ts.logger.Error("Failed to send test notification", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.logger.Info("Test notification sent successfully", "title", title)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdateNotification 测试更新通知
|
||||||
|
func (ts *TestService) TestUpdateNotification() error {
|
||||||
|
// 设置badge
|
||||||
|
if err := ts.TestBadge("●"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送更新通知
|
||||||
|
return ts.TestNotification(
|
||||||
|
"Voidraft Update Available",
|
||||||
|
"New version available",
|
||||||
|
"New version 1.2.3 available (current: 1.2.0)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAll 清除所有测试状态
|
||||||
|
func (ts *TestService) ClearAll() error {
|
||||||
|
// 移除badge
|
||||||
|
if err := ts.TestBadge(""); err != nil {
|
||||||
|
ts.logger.Error("Failed to clear badge during cleanup", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.logger.Info("Test states cleared successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
func SetupSystemTray(app *application.App, mainWindow *application.WebviewWindow, assets embed.FS, trayService *services.TrayService) {
|
func SetupSystemTray(app *application.App, mainWindow *application.WebviewWindow, assets embed.FS, trayService *services.TrayService) {
|
||||||
// 创建系统托盘
|
// 创建系统托盘
|
||||||
systray := app.SystemTray.New()
|
systray := app.SystemTray.New()
|
||||||
|
// 设置提示
|
||||||
|
systray.SetTooltip("voidraft")
|
||||||
// 设置标签
|
// 设置标签
|
||||||
systray.SetLabel("voidraft")
|
systray.SetLabel("voidraft")
|
||||||
systray.Label()
|
|
||||||
// 设置图标
|
// 设置图标
|
||||||
iconBytes, _ := assets.ReadFile("appicon.png")
|
iconBytes, _ := assets.ReadFile("appicon.png")
|
||||||
systray.SetIcon(iconBytes)
|
systray.SetIcon(iconBytes)
|
||||||
|
|||||||
@@ -31,6 +31,19 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
REM Sync remote tags
|
||||||
|
echo [INFO] Syncing remote tags...
|
||||||
|
echo [INFO] Deleting local tags...
|
||||||
|
for /f "delims=" %%i in ('git tag -l') do git tag -d %%i >nul 2>&1
|
||||||
|
|
||||||
|
echo [INFO] Fetching remote tags...
|
||||||
|
git fetch origin --prune >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARNING] Failed to fetch from remote, using local tags
|
||||||
|
) else (
|
||||||
|
echo [INFO] Remote tags synced successfully
|
||||||
|
)
|
||||||
|
|
||||||
REM Get latest git tag
|
REM Get latest git tag
|
||||||
git describe --abbrev=0 --tags > temp_tag.txt 2>nul
|
git describe --abbrev=0 --tags > temp_tag.txt 2>nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ else
|
|||||||
echo "[ERROR] Not in a git repository"
|
echo "[ERROR] Not in a git repository"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
|
# 同步远程标签
|
||||||
|
echo "[INFO] Syncing remote tags..."
|
||||||
|
echo "[INFO] Deleting local tags..."
|
||||||
|
git tag -l | xargs git tag -d &> /dev/null
|
||||||
|
|
||||||
|
echo "[INFO] Fetching remote tags..."
|
||||||
|
if git fetch origin --prune &> /dev/null; then
|
||||||
|
echo "[INFO] Remote tags synced successfully"
|
||||||
|
else
|
||||||
|
echo "[WARNING] Failed to fetch from remote, using local tags"
|
||||||
|
fi
|
||||||
|
|
||||||
# 获取最新的git标签
|
# 获取最新的git标签
|
||||||
LATEST_TAG=$(git describe --abbrev=0 --tags 2>/dev/null)
|
LATEST_TAG=$(git describe --abbrev=0 --tags 2>/dev/null)
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VERSION=1.3.5
|
VERSION=1.3.7
|
||||||
|
|||||||
Reference in New Issue
Block a user