Skip to content

Commit bbf0ad8

Browse files
committed
refactor: optimizes the handling of extension settings and adapts them to the JavaScript review standard for GNOME extensions
1 parent ff695c9 commit bbf0ad8

5 files changed

Lines changed: 153 additions & 40 deletions

File tree

esbuild.ts

Lines changed: 137 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ interface ExtensionMetadata {
1818
uuid: string;
1919
}
2020

21+
const currentDir = dirname(fileURLToPath(import.meta.url));
22+
const metadataPath = resolve(currentDir, 'metadata.json');
23+
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8')) as ExtensionMetadata;
24+
2125
// Externalize local .ts imports and rewrite paths to .js
2226
const localExternals: Plugin = {
2327
name: 'local-externals',
@@ -34,9 +38,137 @@ const localExternals: Plugin = {
3438
},
3539
};
3640

37-
const currentDir = dirname(fileURLToPath(import.meta.url));
38-
const metadataPath = resolve(currentDir, 'metadata.json');
39-
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8')) as ExtensionMetadata;
41+
// Resolve @girs/* imports directly to gi:// and resource:// runtime paths,
42+
// bypassing node_modules to avoid re-export shims and version query strings
43+
const girsResolver: Plugin = {
44+
name: 'girs-resolver',
45+
setup(ctx) {
46+
// @girs/gjs is a type-only shim; drop it entirely
47+
ctx.onResolve({ filter: /^@girs\/gjs$/ }, () => ({
48+
path: 'gjs',
49+
namespace: 'girs-empty',
50+
}));
51+
ctx.onLoad({ filter: /.*/, namespace: 'girs-empty' }, () => ({
52+
contents: '',
53+
loader: 'js',
54+
}));
55+
56+
// @girs/gnome-shell/* → resource:///org/gnome/shell/*.js
57+
ctx.onResolve({ filter: /^@girs\/gnome-shell\// }, (args) => {
58+
const subpath = args.path.replace('@girs/gnome-shell/', '');
59+
const distFile = resolve(
60+
currentDir,
61+
'node_modules/@girs/gnome-shell/dist',
62+
`${subpath}.js`,
63+
);
64+
// Read the actual resource path from the package when the file exists,
65+
// otherwise fall back to the standard resource path pattern
66+
if (existsSync(distFile)) {
67+
const content = readFileSync(distFile, 'utf8');
68+
const match = content.match(/from ['"](.+)['"]/);
69+
if (match) return { path: match[1], external: true };
70+
}
71+
return {
72+
path: `resource:///org/gnome/shell/${subpath}.js`,
73+
external: true,
74+
};
75+
});
76+
77+
// @girs/<name>-<version> → gi://<Namespace> (without ?version=)
78+
ctx.onResolve({ filter: /^@girs\// }, (args) => {
79+
if (args.path === '@girs/gjs' || args.path.startsWith('@girs/gnome-shell/'))
80+
return;
81+
const pkgName = args.path.replace('@girs/', '');
82+
const mainFile = resolve(
83+
currentDir,
84+
`node_modules/@girs/${pkgName}/${pkgName}.js`,
85+
);
86+
if (existsSync(mainFile)) {
87+
const content = readFileSync(mainFile, 'utf8');
88+
const match = content.match(/from ["'](gi:\/\/[^?"']+)/);
89+
if (match) return { path: match[1], external: true };
90+
}
91+
});
92+
},
93+
};
94+
95+
// JS keywords that should NOT be treated as method declarations
96+
const JS_KEYWORDS = new Set([
97+
'if', 'for', 'while', 'switch', 'return', 'throw', 'try', 'catch',
98+
'finally', 'else', 'do', 'new', 'typeof', 'void', 'delete', 'await',
99+
'yield', 'debugger', 'with', 'break', 'continue', 'case', 'default',
100+
'super', 'this', 'true', 'false', 'null', 'undefined',
101+
]);
102+
103+
// Add blank lines between class methods, between the last field and first
104+
// method, and between top-level declarations. esbuild strips these blank
105+
// lines during compilation and prettier does not re-add them.
106+
function addBlankLinesBetweenMembers(code: string): string {
107+
const lines = code.split('\n');
108+
const result: string[] = [];
109+
110+
const isMethodDecl = (trimmed: string): boolean => {
111+
const m = trimmed.match(
112+
/^(?:(?:get |set |static |async )*([a-zA-Z_$#]\w*)\s*\()/,
113+
);
114+
return m !== null && !JS_KEYWORDS.has(m[1]);
115+
};
116+
117+
const isComment = (trimmed: string): boolean =>
118+
trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*');
119+
120+
for (let i = 0; i < lines.length; i++) {
121+
if (i > 0 && lines[i].trim() !== '' && lines[i - 1].trim() !== '') {
122+
const prev = lines[i - 1];
123+
const curr = lines[i];
124+
const prevTrimmed = prev.trimStart();
125+
const currTrimmed = curr.trimStart();
126+
const prevIndent = prev.search(/\S|$/);
127+
const currIndent = curr.search(/\S|$/);
128+
let needsBlank = false;
129+
130+
const isClosingBrace = /^\};?$/.test(prevTrimmed);
131+
132+
// Between methods: closing } at indent N → method decl at indent N
133+
if (
134+
isClosingBrace &&
135+
currIndent === prevIndent &&
136+
(isMethodDecl(currTrimmed) || isComment(currTrimmed))
137+
) {
138+
needsBlank = true;
139+
}
140+
141+
// Between last field and first method at the same indent level
142+
if (
143+
!needsBlank &&
144+
/;$/.test(prevTrimmed) &&
145+
/^[a-zA-Z_$#]\w*/.test(prevTrimmed) &&
146+
currIndent === prevIndent &&
147+
isMethodDecl(currTrimmed)
148+
) {
149+
needsBlank = true;
150+
}
151+
152+
// Between top-level declarations (indent 0) after block closures
153+
if (
154+
!needsBlank &&
155+
currIndent === 0 &&
156+
/[})\]];?\s*$/.test(prevTrimmed) &&
157+
/^(?:var |let |const |function |class |export )/.test(currTrimmed)
158+
) {
159+
needsBlank = true;
160+
}
161+
162+
if (needsBlank) {
163+
result.push('');
164+
}
165+
}
166+
167+
result.push(lines[i]);
168+
}
169+
170+
return result.join('\n');
171+
}
40172

41173
const srcDir = resolve(currentDir, 'src');
42174
const entryPoints = (readdirSync(srcDir, { recursive: true }) as string[])
@@ -49,7 +181,7 @@ const sharedOptions = {
49181
outdir: 'dist',
50182
outbase: 'src',
51183
bundle: true,
52-
plugins: [localExternals],
184+
plugins: [localExternals, girsResolver],
53185
// Do not remove the functions `enable()`, `disable()` and `init()`
54186
treeShaking: false,
55187
// firefox60 // Since GJS 1.53.90
@@ -59,10 +191,6 @@ const sharedOptions = {
59191
// firefox102 // Since GJS 1.73.2
60192
target: 'firefox102' as const,
61193
format: 'esm' as const,
62-
alias: {
63-
// Not exported in @girs/gnome-shell v49; map directly to runtime resource
64-
'@girs/gnome-shell/ui/dash': 'resource:///org/gnome/shell/ui/dash.js',
65-
},
66194
external: ['gi://*', 'resource://*', 'system', 'gettext', 'cairo'],
67195
};
68196

@@ -91,7 +219,7 @@ Promise.all(
91219
const filePath = resolve(distDir, file);
92220
const content = readFileSync(filePath, 'utf8');
93221
const formatted = await format(content, { parser: 'babel', filepath: filePath });
94-
writeFileSync(filePath, formatted);
222+
writeFileSync(filePath, addBlankLinesBetweenMembers(formatted));
95223
}
96224

97225
copyFileSync(metadataPath, metaDist);

src/extension.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const MODULE_FACTORIES: Record<string, () => Module> = {
2828
export default class AuroraShellExtension extends Extension {
2929
private _modules: Map<string, Module> = new Map();
3030
private _settings: Gio.Settings | null = null;
31-
private _settingsHandlers: number[] = [];
3231

3332
override enable(): void {
3433
console.log('Enabling extension');
@@ -60,12 +59,15 @@ export default class AuroraShellExtension extends Extension {
6059
private _connectSettings(): void {
6160
if (!this._settings) return;
6261

62+
const args: any[] = [];
6363
for (const def of MODULE_REGISTRY) {
64-
const handlerId = this._settings.connect(`changed::${def.settingsKey}`, () => {
64+
args.push(`changed::${def.settingsKey}`, () => {
6565
this._toggleModule(def);
6666
});
67-
this._settingsHandlers.push(handlerId);
6867
}
68+
args.push(this);
69+
70+
this._settings.connectObject(...args);
6971
}
7072

7173
private _toggleModule(def: ModuleDefinition): void {
@@ -95,12 +97,7 @@ export default class AuroraShellExtension extends Extension {
9597
override disable(): void {
9698
console.log('Aurora Shell: Disabling extension');
9799

98-
if (this._settings) {
99-
for (const handlerId of this._settingsHandlers) {
100-
this._settings.disconnect(handlerId);
101-
}
102-
this._settingsHandlers = [];
103-
}
100+
this._settings?.disconnectObject(this);
104101

105102
for (const [name, module] of this._modules) {
106103
try {

src/modules/dock.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ type ManagedDockBinding = {
4444
intellihide: InstanceType<typeof DockIntellihide>;
4545
hotArea: InstanceType<typeof DockHotArea> | null;
4646
autoHideReleaseId: number;
47-
destroyed: boolean;
4847
hotAreaActive: boolean;
4948
};
5049

@@ -160,7 +159,6 @@ const DockIntellihide = GObject.registerClass({
160159
private _status: OverlapStatus = OverlapStatus.CLEAR;
161160
private _focusActor: any = null;
162161
private _focusActorId = 0;
163-
private _destroyed = false;
164162

165163
_init(monitorIndex: number) {
166164
super._init();
@@ -195,9 +193,6 @@ const DockIntellihide = GObject.registerClass({
195193
}
196194

197195
override destroy(): void {
198-
if (this._destroyed) return;
199-
this._destroyed = true;
200-
201196
this._disconnectFocusActor();
202197
global.display.disconnectObject(this);
203198
Main.layoutManager.disconnectObject(this);
@@ -206,8 +201,6 @@ const DockIntellihide = GObject.registerClass({
206201
Main.keyboard.disconnectObject(this);
207202
Main.overview.disconnectObject(this);
208203
this.disconnectObject?.(this);
209-
210-
(this as unknown as { run_dispose?: () => void }).run_dispose?.();
211204
}
212205

213206
private _checkOverlap(): void {
@@ -388,7 +381,6 @@ export class Dock extends Module {
388381
intellihide,
389382
hotArea: null,
390383
autoHideReleaseId: 0,
391-
destroyed: false,
392384
hotAreaActive: false,
393385
};
394386

@@ -469,10 +461,11 @@ export class Dock extends Module {
469461
}
470462

471463
private _destroyBinding(binding: ManagedDockBinding): void {
472-
if (binding.destroyed) return;
473-
binding.destroyed = true;
464+
if (binding.autoHideReleaseId) {
465+
GLib.source_remove(binding.autoHideReleaseId);
466+
binding.autoHideReleaseId = 0;
467+
}
474468

475-
this._clearHotAreaReveal(binding);
476469
binding.intellihide.disconnectObject?.(this);
477470
binding.hotArea?.disconnectObject?.(this);
478471

src/modules/pipOnTop.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ export class PipOnTop extends Module {
6363
}
6464

6565
private _disconnectWorkspace(): void {
66-
if (!this._lastWorkspace) return;
67-
6866
if (this._windowAddedId) {
6967
this._lastWorkspace.disconnect(this._windowAddedId);
7068
this._windowAddedId = 0;

src/ui/dash.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ interface AuroraDashParams {
4040
* - The **dash** (this widget) always sits at local y=0 inside the container.
4141
* Show/hide animations use `translation_y` only — never modify `this.y`.
4242
*/
43-
@GObject.registerClass
44-
export class AuroraDash extends Dash {
43+
export const AuroraDash = GObject.registerClass(
44+
class AuroraDashClass extends Dash {
4545
private declare _monitorIndex: number;
4646
private _workArea: DashBounds | null = null;
4747
private _container: St.Bin | null = null;
@@ -55,7 +55,6 @@ export class AuroraDash extends Dash {
5555
private _targetBoxListener: TargetBoxListener | null = null;
5656
private _pendingShow: { animate: boolean; onComplete?: () => void } | null = null;
5757
private _cycleState: { appId: string; windows: any[]; index: number } | null = null;
58-
private _isDestroyed = false;
5958

6059
_init(params: AuroraDashParams = {}): void {
6160
super._init();
@@ -115,9 +114,6 @@ export class AuroraDash extends Dash {
115114
}
116115

117116
override destroy(): void {
118-
if (this._isDestroyed) return;
119-
this._isDestroyed = true;
120-
121117
this._clearAllTimeouts();
122118

123119
(this as any).showAppsButton?.disconnectObject?.(this);
@@ -137,7 +133,6 @@ export class AuroraDash extends Dash {
137133
}
138134

139135
override _queueRedisplay(): void {
140-
if (this._isDestroyed) return;
141136
super._queueRedisplay();
142137
}
143138

@@ -651,7 +646,7 @@ export class AuroraDash extends Dash {
651646
if (this._workAreaUpdateId) return;
652647
this._workAreaUpdateId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
653648
this._workAreaUpdateId = 0;
654-
if (!this._isDestroyed && this._workArea) {
649+
if (this._workArea) {
655650
this.applyWorkArea(this._workArea);
656651
}
657652
return GLib.SOURCE_REMOVE;
@@ -671,7 +666,9 @@ export class AuroraDash extends Dash {
671666
this._clearTimeout('_blockAutoHideDelayId');
672667
this._clearTimeout('_workAreaUpdateId');
673668
}
674-
}
669+
});
670+
671+
export type AuroraDash = InstanceType<typeof AuroraDash>;
675672

676673
function boundsEqual(a: DashBounds | null, b: DashBounds | null): boolean {
677674
if (a === b) return true;

0 commit comments

Comments
 (0)