Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
# Copilot Instructions for SuperOffice Language Tools

# Project coding standards for TypeScript

Apply the [general coding guidelines](./general-coding.instructions.md) to all code.

## TypeScript Guidelines
- Use TypeScript for all new code
- Follow functional programming principles where possible
- Use interfaces for data structures and type definitions
- Prefer immutable data (const, readonly)
- Use optional chaining (?.) and nullish coalescing (??) operators
- Explicit function return types, no-any.

## Repository Overview

VS Code extensions for TypeScript and CRMScript in SuperOffice. Implements LSP servers using Volar (TypeScript) and Langium (CRMScript).
Monorepo containing VS Code extensions for:
1. Core functionality in vscode
2. LSP server using Volar (TypeScript)
3. LSP server using Langium (CRMScript)

**Type:** Monorepo (pnpm workspace) | **Languages:** TypeScript | **Frameworks:** Volar, Langium, VS Code Extension API | **Size:** ~521 TS files | **Runtime:** Node.js 20.x | **Package Manager:** pnpm 8.x (REQUIRED)

Expand Down Expand Up @@ -58,10 +73,6 @@ pnpm run test:core # Fails offline: "getaddrinfo ENOTFOUND update.code.visua
pnpm eslint .
```

**Pre-existing Issues:** 22 ESLint errors (missing return types) - DO NOT block on these.
**Strict Rules:** Explicit function return types, no-any.
**Before commit:** Run `pnpm eslint .` - ensure NO NEW errors.

## Project Structure

### Monorepo Layout
Expand Down
17 changes: 17 additions & 0 deletions .github/general-coding.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Project general coding standards

## Naming Conventions
- Use PascalCase for component names, interfaces, and type aliases
- Use camelCase for variables, functions, and methods
- Prefix private class members with underscore (_)
- Use ALL_CAPS for constants

## Error Handling
- Use try/catch blocks for async operations
- Always log errors with contextual information

## Documentation
- All public methods should have JSDoc

## Git
- Run `pnpm eslint .` - ensure NO NEW errors.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@playwright/test": "^1.56.1",
"@superoffice/webapi": "^11.4.1157",
"@types/node": "latest",
"@types/node": "^24.10.1",
"@types/vscode": "^1.104.0",
"@vitest/coverage-v8": "^4.0.10",
"@volar/language-server": "~2.4.23",
"@volar/vscode": "~2.4.23",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"esbuild": "latest",
"esbuild": "^0.27.0",
"eslint": "^9.37.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.0",
"vitest": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@playwright/test": "^1.49.0"
"vitest": "^4.0.10"
}
}
18 changes: 8 additions & 10 deletions packages/superofficedx-vscode-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "superofficedx-vscode-core",
"type": "module",
"publisher": "superoffice",
"displayName": "SuperOffice Core Tools",
"description": "Provides core functionality for SuperOffice VS Code extensions.",
Expand Down Expand Up @@ -148,16 +147,15 @@
"build": "node ./scripts/build.js"
},
"devDependencies": {
"@playwright/test": "*",
"@types/node": "*",
"@playwright/test": "^1.56.1",
"@types/node": "^24.10.1",
"@types/uuid": "^9.0.5",
"@vitest/coverage-v8": "*",
"@vscode/test-cli": "*",
"@vscode/test-electron": "*",
"esbuild": "*",
"glob": "^10.3.3",
"typescript": "*",
"@vitest/coverage-v8": "^4.0.10",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"esbuild": "^0.27.0",
"typescript": "^5.9.3",
"uuid": "^9.0.1",
"vitest": "*"
"vitest": "^4.0.10"
}
}
115 changes: 48 additions & 67 deletions packages/superofficedx-vscode-core/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, devices } from '@playwright/test';
import { TestOptions } from './tests/workbox';
import { defineConfig } from '@playwright/test';
import { TestOptions } from './tests/e2e/playwright/workbox';

/**
* Read environment variables from file.
Expand All @@ -13,75 +13,56 @@ import { TestOptions } from './tests/workbox';
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig<void, TestOptions>({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
testDir: './tests/e2e/playwright',
/* Run tests in files in parallel */
fullyParallel: false, // Set to false for VS Code tests
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1, // Use single worker for VS Code tests
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Increase timeout for VS Code startup */
timeout: 60000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'VSCode insiders',
use: {
vscodeVersion: 'insiders',
}
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'VSCode insiders',
use: {
vscodeVersion: 'insiders',
}
}
// Remove other browsers for VS Code extension testing
// {
// name: 'chromium',
// use: { ...devices['Desktop Chrome'] },
// },
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
],

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

// import { defineConfig } from '@playwright/test';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ const CALLBACK_URI = `http://${CALLBACK_HOSTNAME}:${CALLBACK_PORT}/callback`;
export interface IAuthenticationService {
login(environment: string): Promise<Token>;
getClaimsFromToken(token: string): UserClaims;
generateAuthorizeUrl(environment: string): Promise<Uri>;
}

export class AuthenticationService implements IAuthenticationService {
private server: http.Server | null = null;

private _pendingStates: string[] = [];
private _codeVerifiers = new Map<string, string>();
private _scopes = new Map<string, string[]>();
private _environment: string = "";
private _state: string = "";

/**
* Load HTML template from resources folder
Expand Down Expand Up @@ -58,45 +59,54 @@ export class AuthenticationService implements IAuthenticationService {
}
}

/**
* Generate AuthorizeUrl with scopes, parameters and state etc.
*/
public async generateAuthorizeUrl(environment: string): Promise<Uri> {


const nonceId = uuid();
const scopes = ['openid']
const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32));
const codeChallenge = this.toBase64UrlEncoding(this.sha256(codeVerifier));
const callbackUri = await env.asExternalUri(Uri.parse(CALLBACK_URI));
const callbackQuery = new URLSearchParams(callbackUri.query);
const state = callbackQuery.get('state') || nonceId;

this._environment = environment;
this._state = state;

this._pendingStates.push(state);
this._codeVerifiers.set(state, codeVerifier);
this._scopes.set(state, scopes);

const searchParams = new URLSearchParams([
['response_type', "code"],
['client_id', CLIENT_ID],
['redirect_uri', CALLBACK_URI],
['state', state],
['scope', scopes.join(' ')],
['code_challenge_method', 'S256'],
['code_challenge', codeChallenge],
]);

const uri = Uri.parse(`https://${environment}.superoffice.com/login/common/oauth/authorize?${searchParams.toString()}`);

return Promise.resolve(uri);
}

/**
* Log in to OpenId Connect
*/
public async login(environment: string): Promise<Token> {
this._environment = environment;

return await window.withProgress<Token>({
location: ProgressLocation.Notification,
title: "Signing in to SuperOffice...",
cancellable: true,

}, async (_, token) => {

const nonceId = uuid();

const scopes = ['openid']

const codeVerifier = this.toBase64UrlEncoding(crypto.randomBytes(32));
const codeChallenge = this.toBase64UrlEncoding(this.sha256(codeVerifier));

const callbackUri = await env.asExternalUri(Uri.parse(CALLBACK_URI));
const callbackQuery = new URLSearchParams(callbackUri.query);
const stateId = callbackQuery.get('state') || nonceId;

this._pendingStates.push(stateId);
this._codeVerifiers.set(stateId, codeVerifier);
this._scopes.set(stateId, scopes);

const searchParams = new URLSearchParams([
['response_type', "code"],
['client_id', CLIENT_ID],
['redirect_uri', CALLBACK_URI],
['state', stateId],
['scope', scopes.join(' ')],
['code_challenge_method', 'S256'],
['code_challenge', codeChallenge],
]);

const uri = Uri.parse(`https://${this._environment}.superoffice.com/login/common/oauth/authorize?${searchParams.toString()}`);
const uri = await this.generateAuthorizeUrl(environment);

await env.openExternal(uri);

Expand All @@ -109,7 +119,7 @@ export class AuthenticationService implements IAuthenticationService {
])
}
finally {
this._pendingStates = this._pendingStates.filter(n => n !== stateId);
this._pendingStates = this._pendingStates.filter(n => n !== this._state);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test, expect } from './workbox';

test('should be able to execute the first test of the example project', async ({ workbox }) => {
// Test that VS Code is running
const title = await workbox.title();
expect(title).toContain('Visual Studio Code');
console.log('VS Code title:', title);

// Wait a bit longer for the extension to activate
await workbox.waitForTimeout(2000);

// Check if VS Code UI has loaded properly
const explorerVisible = await workbox.locator('.explorer-viewlet').isVisible();
console.log('Explorer visible:', explorerVisible);

// Try to open command palette and look for SuperOffice commands
await workbox.keyboard.press('Control+Shift+P');
await workbox.waitForSelector('.quick-input-widget', { timeout: 5000 });

// Type to search for SuperOffice commands
await workbox.fill('.quick-input-widget input', 'SuperOffice');
await workbox.waitForTimeout(1000);

// Check if any SuperOffice commands appear
const quickInputList = await workbox.locator('.quick-input-list');
const commandsText = await quickInputList.textContent();
console.log('Available commands:', commandsText);

// Check if our extension commands are available
const hasSignInCommand = commandsText?.includes('Sign In') || commandsText?.includes('SuperOffice');
console.log('SuperOffice commands found:', hasSignInCommand);

// Press Escape to close command palette
await workbox.keyboard.press('Escape');

// Basic verification that VS Code and extension loaded
expect(title).toContain('Visual Studio Code');
console.log('Test completed successfully');
});
Loading
Loading