diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..1da6debc --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,120 @@ +name: CI +on: + pull_request: + branches: + - v2 + +# Ensure scripts are run with pipefail. See: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference +defaults: + run: + shell: bash + +jobs: + tests: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + with: + node-version: "18.x" + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + + - run: pnpm install + + # Grab localizations + - run: pnpm docs-sync pull microsoft/TypeScript-Website-localizations#main 1 + + # Build the packages + - run: pnpm bootstrap + - run: pnpm build + + # Verify it compiles + - run: pnpm build-site + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' + with: + name: site + path: packages/typescriptlang-org/public + + # Run all the package's tests + - run: pnpm test + + # danger for PR builds + - if: github.event_name == 'pull_request' && github.event.pull_request.base.repo.id == github.event.pull_request.head.repo.id && matrix.os == 'ubuntu-latest' + run: "pnpm danger ci" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: | + git add . + if ! git diff --staged --exit-code --quiet; then + echo "This PR is missing some generated changes. Please update locally or merge the patch artifact." + echo "" + git diff --staged + git diff --staged > missing.patch + exit 1 + fi + name: Check for uncommitted changes + id: check-diff + if: github.event_name == 'pull_request' + + - name: Upload diff artifact + if: ${{ failure() && steps.check-diff.conclusion == 'failure' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: missing.patch + path: missing.patch + + changesets: + name: changesets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 + with: + node-version: 'lts/*' + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + + - run: pnpm install + + - name: Check for missing changesets + run: | + PR_CHANGESETS=$(ls .changeset | (grep -v -E 'README\.md|config\.json' || true) | wc -l) + MAIN_CHANGESETS=$(git ls-tree -r origin/v2 .changeset | (grep -v -E 'README\.md|config\.json' || true) | wc -l) + + # If the PR has no changesets, but main has changesets, assume this is PR is a versioning PR and exit + if [[ $PR_CHANGESETS -eq 0 && $MAIN_CHANGESETS -gt 0 ]]; then + echo "This PR is a versioning PR, exiting" + exit 0 + fi + + # git switch -c changesets-temp + # git checkout origin/v2 -- + pnpm changeset status --since=origin/v2 + + required: + runs-on: ubuntu-latest + if: ${{ always() }} + needs: + - tests + - changesets + + steps: + - name: Check required jobs + env: + NEEDS: ${{ toJson(needs) }} + run: | + ! echo $NEEDS | jq -e 'to_entries[] | { job: .key, result: .value.result } | select(.result != "success")' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..d48ed1c2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,71 @@ +name: 'Code Scanning - Action' + +on: + push: + branches: + - v2 + pull_request: + branches: + - v2 + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + # │ │ │ │ │ + # │ │ │ │ │ + # * * * * * + - cron: '30 1 * * 0' + +permissions: + contents: read + +# Ensure scripts are run with pipefail. See: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference +defaults: + run: + shell: bash + +jobs: + CodeQL-Build: + # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest + runs-on: ubuntu-latest + if: github.repository == 'microsoft/TypeScript-Website' + + permissions: + # required for all workflows + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + with: + config-file: ./.github/codeql/codeql-configuration.yml + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below). + - name: Autobuild + uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # ✏️ If the Autobuild fails above, remove it and uncomment the following + # three lines and modify them (or add more) to build your code if your + # project uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml new file mode 100644 index 00000000..918fd78b --- /dev/null +++ b/.github/workflows/deploy-preview.yml @@ -0,0 +1,188 @@ +name: Deploy preview environment +on: + workflow_run: + workflows: [CI] + types: [completed] + workflow_dispatch: + inputs: + pr: + required: true + type: string + description: PR number + pull_request_target: + types: [labeled] + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Get PR/workflow run info + id: get-info + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + console.dir(context, { depth: null }); + + let pr; + let workflowRun; + let isLabel = false; + switch (context.eventName) { + case "workflow_dispatch": + console.log("Workflow dispatch event"); + pr = (await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: +context.payload.inputs.pr, + })).data; + break; + case "pull_request_target": + console.log("Pull request target event"); + pr = context.payload.pull_request; + break; + case "workflow_run": + console.log("Workflow run event"); + workflowRun = context.payload.workflow_run; + pr = workflowRun.pull_requests[0]; + if (pr) { + console.log("PR found in workflow run"); + // Reload the PR to get the labels. + pr = (await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + })).data; + } else { + const q = `is:pr is:open repo:${context.repo.owner}/${context.repo.repo} sha:${workflowRun.head_sha}`; + console.log(`PR not found in workflow run, searching for it with ${q}`); + // PRs sent from forks do not get the pull_requests field set. + // https://github.com/orgs/community/discussions/25220 + const response = await github.rest.search.issuesAndPullRequests({ q }); + if (response.data.items.length > 0) { + pr = response.data.items[0]; + } + } + } + + if (!pr) { + console.log("Could not find PR"); + return null; + } + + console.log(`Found PR ${pr.html_url}`); + console.dir(pr, { depth: null }); + + if (pr.state !== "open") { + console.log(`PR ${pr.number} is not open`); + return null; + } + + if (!pr.labels.some((label) => label.name === "deploy-preview")) { + console.log(`PR ${pr.number} does not have the deploy-preview label`); + return null; + } + + if (!workflowRun) { + console.log(`No workflow run found in event, searching for it with ${pr.head.sha}`); + try { + workflowRun = (await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "CI.yml", + head_sha: pr.head.sha, + per_page: 1, + })).data.workflow_runs[0]; + } catch (e) { + console.log(e); + } + } + + if (!workflowRun) { + console.log(`Could not find workflow run for PR ${pr.number}`); + return null; + } + + console.log(`Found workflow run ${workflowRun.html_url}`); + console.dir(workflowRun, { depth: null }); + + try { + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: "microsoft", + repo: "TypeScript-Website", + run_id: workflowRun.id, + }); + console.dir(artifacts, { depth: null }); + if (!artifacts.data.artifacts.some(x => x.name === "site")) { + console.log("No artifact found in workflow run"); + return null; + } + } catch (e) { + console.log(e); + return null; + } + + const result = { pr: pr.number, runId: workflowRun.id }; + console.log(result); + return result; + + - name: Download site build from PR + if: ${{ steps.get-info.outputs.result != 'null' }} + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + with: + name: site + path: site + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ fromJson(steps.get-info.outputs.result).runId }} + + - name: Deploy + id: deploy + if: ${{ steps.get-info.outputs.result != 'null' }} + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PREVIEW }} + # Unsetting the repo token so we can control commenting ourselves. + # repo_token: ${{ secrets.GITHUB_TOKEN }} + action: "upload" + app_location: "site" + skip_app_build: true + production_branch: v2 + deployment_environment: ${{ fromJson(steps.get-info.outputs.result).pr }} + + - name: Comment on PR + if: ${{ steps.get-info.outputs.result != 'null' }} + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + PR_NUMBER: ${{ fromJson(steps.get-info.outputs.result).pr }} + SITE_URL: ${{ steps.deploy.outputs.static_web_app_url }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prefix = "Azure Static Web Apps: Your stage site is ready!"; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: +process.env.PR_NUMBER, + per_page: 100, + }); + + for (const comment of comments) { + if (comment.user?.login === "github-actions[bot]" && comment.body?.startsWith(prefix)) { + console.log(`Deleting comment ${comment.id}`); + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }); + } + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: +process.env.PR_NUMBER, + body: `${prefix} Visit it here: ${process.env.SITE_URL}` + }); diff --git a/packages/documentation/copy/en/modules-reference/Reference.md b/packages/documentation/copy/en/modules-reference/Reference.md index 20fd7adf..082d8918 100644 --- a/packages/documentation/copy/en/modules-reference/Reference.md +++ b/packages/documentation/copy/en/modules-reference/Reference.md @@ -295,6 +295,7 @@ While it’s rare to need to mix imports and require calls in the same file, thi #### Examples ```ts +// @Filename: main.ts import x, { y, z } from "mod"; import mod = require("mod"); const dynamic = import("mod"); @@ -304,6 +305,7 @@ export default "default export"; ``` ```js +// @Filename: main.js import x, { y, z } from "mod"; const mod = require("mod"); const dynamic = import("mod"); diff --git a/packages/documentation/copy/en/modules-reference/Theory.md b/packages/documentation/copy/en/modules-reference/Theory.md index 270de633..ddcb5103 100644 --- a/packages/documentation/copy/en/modules-reference/Theory.md +++ b/packages/documentation/copy/en/modules-reference/Theory.md @@ -254,7 +254,7 @@ Remember the three components of TypeScript’s [job](#typescripts-job-concernin 2. Ensure that imports in those **outputs** will **resolve successfully** 3. Know what **type** to assign to **imported names**. -Module resolution is needed to accomplish last two. But when we spend most of our time working in input files, it can be easy to forget about (2)—that a key component of module resolution is validating that the imports or `require` calls in the output files, containing the [same module specifiers as the input files](#module-specifiers-are-not-transformed), will actually work at runtime. Let’s look at a new example with multiple files: +Module resolution is needed to accomplish last two. But when we spend most of our time working in input files, it can be easy to forget about (2)—that a key component of module resolution is validating that the imports or `require` calls in the output files, containing the [same module specifiers as the input files](#module-specifiers-are-not-transformed-by-default), will actually work at runtime. Let’s look at a new example with multiple files: ```ts // @Filename: math.ts @@ -296,7 +296,7 @@ Node.js ESM `import` declarations use a strict module resolution algorithm that ![A flowchart diagram with identical structure to the one above. There are two groups of files: Input files and Output files. src/main.mts (an input file) maps to output file dist/main.mjs, which resolves through module specifier "./math.mjs" to dist/math.mjs (another output file), which maps back to input file src/math.mts.](./diagrams/theory.md-3.svg) -Understanding this mental model may not immediately eliminate the strangeness of seeing output file extensions in input files, and it’s natural to think in terms of shortcuts: _`"./math.mjs"` refers to the input file `math.mts`. I have to write the output extension, but the compiler knows to look for `.mts` when I write `.mjs`._ This shortcut is even how the compiler works internally, but the more robust mental model explains _why_ module resolution in TypeScript works this way: given the constraint that the module specifier in the output file will be [the same](#module-specifiers-are-not-transformed) as the module specifier in the input file, this is the only process that accomplishes our two goals of validating output files and assigning types. +Understanding this mental model may not immediately eliminate the strangeness of seeing output file extensions in input files, and it’s natural to think in terms of shortcuts: _`"./math.mjs"` refers to the input file `math.mts`. I have to write the output extension, but the compiler knows to look for `.mts` when I write `.mjs`._ This shortcut is even how the compiler works internally, but the more robust mental model explains _why_ module resolution in TypeScript works this way: given the constraint that the module specifier in the output file will be [the same](#module-specifiers-are-not-transformed-by-default) as the module specifier in the input file, this is the only process that accomplishes our two goals of validating output files and assigning types. ### The role of declaration files