diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..8f205f60de --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,77 @@ +# Copilot Instructions for microsoft-teams-library-js + +## Code Style + +- TypeScript with single quotes, 2-space indentation, semicolons, trailing commas, LF line endings, 120 char print width +- Explicit return types on all functions (`@typescript-eslint/explicit-function-return-type`) +- Sorted imports (`simple-import-sort`) +- Prefix unused variables with `_` (e.g., `_unusedParam`) +- Use `curly` braces for all control flow — no braceless `if`/`else` + +## Design Principles + +- **Descriptive variable names** — use clear and descriptive names for variables, functions, and classes to improve readability and maintainability. Always prefer clarity over brevity. +- **One representation of state** — each piece of state should have exactly one canonical representation. Never store the same state in two places or two forms. +- **Strong types over strings** — use enums, interfaces, and union types instead of raw strings. If a value has a known set of options, model it as a type. +- **Zero tolerance for repetition** — one repetition is one too many. Extract shared logic, constants, and patterns immediately. +- **Arrays: empty vs null** — use empty arrays (`[]`) to mean "none", not `null` or `undefined`. Reserve `null` for "not yet loaded" or "not applicable". +- **Units in variable names** — variables holding numbers with units must include the unit in the name (e.g., `timeoutInMs`, `fileSizeInBytes`, `durationInSeconds`). +- **Centralize extensible definitions** — when you have a class of things that people will add to over time, put all the data in one place (a single registry, map, or config). Don't spread definitions across the codebase. Make it impossible to add a new entry without defining all required fields (use TypeScript interfaces to enforce completeness). + +## SDK Capability Pattern + +Every public capability in `packages/teams-js/src/public/` must: +- Export an `isSupported()` function that checks `ensureInitialized(runtime) && runtime.supports.{capability}` +- Call `ensureInitialized(runtime, ...allowedFrameContexts)` before any operation. Newly added capabilities should never specify allowed frame contexts so as to use the runtime as the source of truth for supported capabilities. +- Tag all API calls with telemetry: `getApiVersionTag(versionNumber, ApiName.Capability_Method)` +- Return `Promise` for async operations +- Export parameter/result interfaces from the same module +- Throw `errorNotSupportedOnPlatform` when `isSupported()` is false + +## Testing + +- Test files mirror `src/` structure under `test/` +- Use the `Utils` helper class for mock window setup +- Every capability test must cover: + - Calls before initialization throw `errorLibraryNotInitialized` + - Each `FrameContexts` value (allowed vs disallowed) + - `isSupported()` returns false when runtime is not initialized +- Use `beforeEach` to initialize with `app._initialize(utils.mockWindow)` and `afterEach` to call `app._uninitialize()` + +## Contribution Rules + +- Run `pnpm changefile` before submitting a PR — CI will fail without a beachball changefile +- Change types: `minor` (new feature), `patch` (bug fix), `none` (no published impact). Major and prerelease are disallowed. +- Changefile descriptions use past tense and backtick-wrap API names (e.g., "Added `calendar.openCalendarItem`") +- One changefile per PR only +- Bundle size for `{ app, authentication, pages }` import must stay under the limit specified in `package.json` — if your change exceeds this, investigate tree-shaking or justify the increase + +## Project Structure + +- `packages/teams-js/` — core SDK, the only published package +- `packages/teams-js/src/public/` — public API capabilities +- `packages/teams-js/src/private/` — internal/experimental APIs (copilot, externalAppAuth, etc.) +- `packages/teams-js/src/internal/` — shared utilities (communication, handlers, telemetry) +- `apps/` — test applications (not published) +- `tools/cli/` — utility scripts for bundle analysis and releases + +## Change Organization + +- **One logical change per PR** — each pull request should represent a single coherent idea: one new capability, one refactor, one bug fix. If a task requires both a refactor and a new feature, split them into separate PRs with the refactor landing first. +- **Reviewable size** — aim for PRs that a reviewer can hold in their head at once. If a PR touches more than ~10 files or ~400 lines of diff, look for natural seams to split it. Common split points: infrastructure/types first, then implementation, then tests for new behavior, then documentation. +- **Commits tell a story** — within a PR, each commit should compile and pass tests. Order commits so reviewers can follow the progression: types/interfaces → implementation → tests → wiring/integration. +- **Separate mechanical from meaningful** — keep automated or mechanical changes (renames, import reordering, lint fixes, file moves) in their own commits or PRs so reviewers can skip them quickly and focus on behavioral changes. + +## PR Review Guidelines + +- **Propose splitting large PRs** — when reviewing a PR that is large (roughly >400 lines of meaningful diff) or crosses multiple unrelated concerns, suggest a concrete split plan. Name the proposed child PRs, what files/changes go in each, and the order they should land. Frame the suggestion constructively: explain how splitting will speed up review and reduce merge risk. +- **Evaluate cohesion** — a good PR changes things that change for the same reason. Flag changes that bundle unrelated work (e.g., a bug fix mixed with a feature, or a refactor mixed with new API surface). Each separable concern should be its own PR. +- **Identify safe landing order** — when proposing a split, suggest the dependency-safe merge order (e.g., "Land the type definitions first, then the implementation PR can build on them"). Ensure each proposed sub-PR is independently shippable and leaves the codebase in a working state. + +## Build Commands + +- `pnpm build` — build everything (from repo root) +- `cd packages/teams-js && pnpm build` — build SDK only +- `cd packages/teams-js && pnpm test` — run SDK tests +- `cd packages/teams-js && pnpm lint` — lint SDK with auto-fix +- `cd packages/teams-js && pnpm size` — check bundle size limits diff --git a/.github/skills/teams-js-contributor/SKILL.md b/.github/skills/teams-js-contributor/SKILL.md new file mode 100644 index 0000000000..1e302a1fa3 --- /dev/null +++ b/.github/skills/teams-js-contributor/SKILL.md @@ -0,0 +1,326 @@ +--- +name: teams-js-contributor +description: "Guide for contributing to the TeamsJS SDK monorepo. Use when user asks to build, test, lint, add capabilities, create changefiles, fix bugs, or understand the monorepo structure. Mentions: 'teams-js', 'TeamsJS', '@microsoft/teams-js', 'changefile', 'beachball', 'bundle size', 'tree-shaking', 'test app'." +metadata: + version: '1.0.0' + tool_type: monorepo + requires: Node.js 18+, pnpm 9.0.6+ +--- + +# TeamsJS SDK Contributor Guide + +Agent skill for navigating, building, testing, and contributing to the `microsoft-teams-library-js` monorepo — the Microsoft Teams JavaScript client library (`@microsoft/teams-js`). + +## When to Use This Skill + +**Triggers — activate this skill when:** + +- User asks to build, test, or lint the TeamsJS SDK +- User asks how to add a new capability or public API +- User asks about monorepo structure (packages, apps, tools) +- User needs to create a beachball changefile for a PR +- User asks about bundle size limits or tree-shaking +- User mentions "teams-js", "TeamsJS", or "@microsoft/teams-js" +- User asks how to contribute or submit a PR +- User asks about running test apps (test app, perf app, SSR app, Blazor app) +- User asks about the SDK's runtime capability checks or `isSupported()` pattern + +**Anti-triggers — do NOT use this skill when:** + +- User is building an app _with_ TeamsJS as a consumer (point them to https://learn.microsoft.com/javascript/api/overview/msteams-client) +- User asks about Teams Bot Framework or other Teams SDKs + +## Monorepo Structure + +``` +microsoft-teams-library-js/ +├── packages/teams-js/ # Core SDK — @microsoft/teams-js +│ ├── src/ +│ │ ├── public/ # Public API capabilities (calendar, clipboard, dialog, etc.) +│ │ ├── private/ # Internal/experimental APIs (copilot, externalAppAuth, etc.) +│ │ ├── internal/ # Shared utilities (communication, handlers, telemetry, utils) +│ │ └── index.ts # Package entry point +│ ├── test/ # Jest tests mirroring src/ structure +│ ├── dist/ +│ │ ├── esm/ # ES modules (tree-shakable, preserveModules) +│ │ └── umd/ # UMD bundle (MicrosoftTeams.min.js) +│ ├── rollup.config.mjs # Build config (ESM + UMD outputs) +│ └── package.json +├── apps/ +│ ├── teams-test-app/ # Functional test app for SDK APIs +│ ├── teams-perf-test-app/ # Performance/loading time test app +│ ├── ssr-test-app/ # Server-side rendering test app +│ ├── blazor-test-app/ # Blazor integration test app +│ └── tree-shaking-test-app/ # Tree-shaking verification app +├── tools/ +│ └── cli/ # Utility scripts (bundle analysis, pre-release, etc.) +├── change/ # Beachball change files (auto-generated) +├── beachball.config.js # Changefile configuration +├── jest.config.common.js # Shared Jest config +└── tsconfig.common.json # Shared TypeScript config +``` + +## Command Reference + +### Build + +```bash +# Build everything (SDK + all apps) +pnpm build + +# Build only the SDK (faster — run from packages/teams-js) +cd packages/teams-js && pnpm build + +# Build SDK without lint/docs/size checks (fastest) +cd packages/teams-js && pnpm build-rollup +``` + +The SDK `build` script runs: `clean → lint → build-rollup → build-webpack → docs:validate → size` + +### Test + +```bash +# Run all tests across the monorepo +pnpm test + +# Run only SDK tests (faster — from packages/teams-js) +cd packages/teams-js && pnpm test + +# Run a specific test file +cd packages/teams-js && npx jest test/public/calendar.spec.ts + +# Run tests matching a pattern +cd packages/teams-js && npx jest --testPathPattern="clipboard" + +# Run with verbose output +cd packages/teams-js && pnpm test:verbose +``` + +### Lint + +```bash +# Lint everything +pnpm lint + +# Lint SDK only (with auto-fix) +cd packages/teams-js && pnpm lint +``` + +### Bundle Size + +```bash +# Check bundle size against limits +cd packages/teams-js && pnpm size + +# Full bundle analysis +pnpm bundleAnalyze +``` + +**Size limits** are defined in the root `package.json` under `size-limit`. The key constraint: + +- Importing `{ app, authentication, pages }` must stay under the limit specified in `package.json` (uncompressed, no brotli) +- If your change exceeds this, either tree-shaking is broken or you must justify the increase in your PR + +### Changefiles (Beachball) + +```bash +# Generate a changefile (REQUIRED before PR) +pnpm changefile + +# Generate without auto-commit +pnpm changefile --no-commit +``` + +**Change types:** + +- **minor** — new backwards-compatible functionality +- **patch** — backwards-compatible bug fix +- **none** — change doesn't affect the published package + +**Major and prerelease are disallowed** per `beachball.config.js`. + +Change descriptions should use past tense and backtick-wrap API names: + +- ✅ `"Added \`calendar.openCalendarItem\` to support calendar deep links"` +- ❌ `"Add calendar feature"` + +### Test Apps + +```bash +pnpm start-test-app # Functional test app (default) +pnpm start-test-app-local # Test app with local SDK build +pnpm start-perf-app # Performance test app +pnpm start-ssr-app # SSR test app +pnpm start-blazor-app # Blazor test app +``` + +### Documentation + +```bash +# Generate TypeDoc reference docs +pnpm docs + +# Validate docs without emitting (runs during build) +pnpm docs:validate +``` + +## Public API Capability Pattern + +Every public capability module in `src/public/` follows this structure: + +```typescript +// src/public/myCapability.ts +import { sendAndHandleSdkError } from '../internal/communication'; +import { ensureInitialized } from '../internal/internalAPIs'; +import { ApiVersionNumber, getApiVersionTag } from '../internal/telemetry'; +import { FrameContexts } from './constants'; +import { runtime } from './runtime'; + +const myCapabilityTelemetryVersionNumber: ApiVersionNumber = ApiVersionNumber.V_2; + +// 1. Export interfaces for parameters/return types +export interface MyParams { + itemId: string; +} + +// 2. Export async function with runtime + context checks +export function doSomething(params: MyParams): Promise { + // a. Validate initialization + ensureInitialized(runtime); + // b. Check capability support + if (!isSupported()) { + throw errorNotSupportedOnPlatform; + } + // c. Validate parameters + if (!params.itemId) { + throw new Error('itemId is required'); + } + // d. Send message to host and handle response + return sendAndHandleSdkError( + getApiVersionTag(myCapabilityTelemetryVersionNumber, ApiName.MyCapability_DoSomething), + 'myCapability.doSomething', + params, + ); +} + +// 3. Export isSupported() — REQUIRED for every capability +export function isSupported(): boolean { + return ensureInitialized(runtime) && runtime.supports.myCapability ? true : false; +} +``` + +**Key rules:** + +- Every capability MUST export an `isSupported()` function +- Use `ensureInitialized(runtime)` before any operation +- Use telemetry version tagging on all API calls +- All async operations return `Promise` +- Parameter interfaces are exported from the same module + +## Test Pattern + +Test files mirror the `src/` structure: + +```typescript +// test/public/myCapability.spec.ts +describe('myCapability', () => { + const utils = new Utils(); + + beforeEach(() => { + utils.processMessage = null; + utils.messages = []; + GlobalVars.frameContext = undefined; + app._initialize(utils.mockWindow); + }); + + afterEach(() => { + app._uninitialize(); + }); + + describe('isSupported', () => { + it('should return false if runtime not initialized', () => { + utils.uninitializeRuntimeConfig(); + expect(myCapability.isSupported()).toBeFalsy(); + }); + }); + + describe('doSomething', () => { + it('should not allow calls before initialization', async () => { + await expect(myCapability.doSomething(params)).rejects.toThrowError(new Error(errorLibraryNotInitialized)); + }); + + // Test each allowed/disallowed FrameContext + const allowedFrameContexts = [FrameContexts.content]; + Object.values(FrameContexts).forEach((frameContext) => { + if (allowedFrameContexts.includes(frameContext)) { + it(`should allow calls from ${frameContext}`, async () => { + await utils.initializeWithContext(frameContext); + // ... test success path + }); + } else { + it(`should not allow calls from ${frameContext}`, async () => { + await utils.initializeWithContext(frameContext); + expect(() => myCapability.doSomething(params)).toThrowError(/* ... */); + }); + } + }); + }); +}); +``` + +## Common Workflows + +### Adding a New Capability + +1. Create `src/public/myCapability.ts` following the capability pattern above. New capabilities should _never_ specify allowed frame contexts in `ensureInitialized()`. +2. Export from `src/public/index.ts` +3. Add `runtime.supports.myCapability` to the runtime interface in `src/public/runtime.ts` +4. Create `test/public/myCapability.spec.ts` following the test pattern +5. Run `pnpm build && pnpm test` from `packages/teams-js` +6. Check bundle size with `pnpm size` +7. Generate changefile: `pnpm changefile` (type: minor) + +### Fixing a Bug in an Existing Capability + +1. Locate the capability in `src/public/{capability}.ts` +2. Make the fix +3. Add/update test in `test/public/{capability}.spec.ts` +4. Run `cd packages/teams-js && pnpm test -- --testPathPattern="{capability}"` +5. Run full build: `cd packages/teams-js && pnpm build` +6. Generate changefile: `pnpm changefile` (type: patch) + +### Submitting a PR + +1. Create a branch from `main`: `git checkout -b issue-123` +2. Make changes and commit +3. Run `pnpm changefile` — **pipeline will fail without this** +4. Push and open PR against `main` +5. CI runs: install → build → test → lint (on Node 18.x and 20.x) + +## Key Files Reference + +| File | Purpose | +| ------------------------------------------------- | --------------------------------------------------------------------- | +| `packages/teams-js/src/public/runtime.ts` | Runtime capability detection and `supports` interface | +| `packages/teams-js/src/public/constants.ts` | `FrameContexts` enum and other shared constants | +| `packages/teams-js/src/internal/communication.ts` | Host-app message passing (core IPC) | +| `packages/teams-js/src/internal/telemetry.ts` | API telemetry tagging with version numbers | +| `packages/teams-js/src/internal/internalAPIs.ts` | `ensureInitialized()` and internal state management | +| `packages/teams-js/src/public/app/app.ts` | `app.initialize()` — SDK entry point | +| `beachball.config.js` | Changefile config (scoped to `packages/teams-js`, ignores tests/docs) | +| `packages/teams-js/rollup.config.mjs` | Build config producing ESM + UMD outputs | + +## Limitations + +**This skill CAN:** + +- ✅ Guide through building, testing, and linting the SDK +- ✅ Explain monorepo structure and module patterns +- ✅ Help add new capabilities following established patterns +- ✅ Guide changefile creation and PR submission + +**This skill CANNOT:** + +- ❌ Help build apps that _consume_ TeamsJS (use official docs instead) +- ❌ Debug Teams host-side issues (SDK sends messages; host interprets them) +- ❌ Manage Azure DevOps pipeline configuration (`azure-pipelines.yml` is separate infra) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..02dd134122 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +.github/copilot-instructions.md \ No newline at end of file