Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ jobs:
# Block publishing when checked-in artifacts don't match the regenerated
# output (i.e. publishing unreviewed content).
- name: Verify checked-in generated artifacts
run: git diff --exit-code -- derived/current generated/current
run: |
git diff --exit-code -- derived/current generated/current
status="$(git status --porcelain --untracked-files=all -- derived/current generated/current)"
if [[ -n "$status" ]]; then
printf '%s\n' "$status"
exit 1
fi

- name: Run validation and smoke tests
run: npm test
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ jobs:
run: npm run generate

- name: Verify checked-in generated artifacts
run: git diff --exit-code -- derived/current generated/current
run: |
git diff --exit-code -- derived/current generated/current
status="$(git status --porcelain --untracked-files=all -- derived/current generated/current)"
if [[ -n "$status" ]]; then
printf '%s\n' "$status"
exit 1
fi

- name: Run standalone validation
run: npm test
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ npm install --save-dev typescript-baseline-lib

Now only the supported Baseline widely available JavaScript surfaces type-check. APIs that haven't reached Baseline yet (`Promise.withResolvers`, `Array.fromAsync` until it promotes, and so on) are reported as errors. The end goal is first-class `--lib baseline` support upstream in TypeScript.

## Allow a polyfilled feature

When the runtime loads an audited polyfill, add its generated web-features entry after the base package. For example, core-js can provide `Promise.withResolvers` at runtime:

```ts
import "core-js/proposals/promise-with-resolvers";
```

```json
{
"compilerOptions": {
"noLib": true,
"types": [
"typescript-baseline-lib",
"typescript-baseline-lib/allow/promise-withresolvers"
]
}
}
```

Only entries approved in `registry/allowlist.json` are public. The registry is a permanent path contract: after every registered compat key becomes Baseline widely available, the same entry remains as an alias to the base package. Limited availability features with `baselineStatus: false` are rejected.

## Current contract

- Target is `baseline` only.
Expand Down
75 changes: 71 additions & 4 deletions deploy/package-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
for (const file of packageConfig.generatedFiles) {
const destinationPath = path.join(stageDirectory, file.to);
await mkdir(path.dirname(destinationPath), { recursive: true });
await cp(file.from, destinationPath);
await cp(file.from, destinationPath, { recursive: true });
}

const packageVersion = versionOverride ?? await resolveNextPackageVersion(packageConfig);
Expand Down Expand Up @@ -231,10 +231,16 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
},
keywords: packageConfig.keywords,
types: "./index.d.ts",
typesVersions: {
"*": {
"allow/*": ["allow/*/index.d.ts"],
},
},
files: [
"index.d.ts",
"baseline.d.ts",
"snapshot.json",
"allow/",
"reports/",
"README.md",
"LICENSE",
Expand Down Expand Up @@ -306,6 +312,11 @@ async function buildReleasePlan(stageSummary) {
}

const removedFiles = [...publishedPaths].sort(compareStrings);
assertNoRemovedAllowEntries(removedFiles);
assertAllowEntryContractsPreserved(
published.snapshot.get("reports/generation.json"),
stagedSnapshot.get("reports/generation.json"),
);
const changed = !published.version || changedFiles.length > 0 || removedFiles.length > 0;

return {
Expand All @@ -327,6 +338,60 @@ async function buildReleasePlan(stageSummary) {
};
}

/**
* @param {string[]} removedFiles
*/
export function assertNoRemovedAllowEntries(removedFiles) {
const removedEntries = removedFiles.filter(relativePath => /^allow\/[^/]+\/index\.d\.ts$/.test(relativePath));
if (removedEntries.length) {
throw new Error(`Published allow entry paths cannot be removed: ${removedEntries.join(", ")}`);
}
}

/**
* @param {string | undefined} publishedReportText
* @param {string | undefined} stagedReportText
*/
export function assertAllowEntryContractsPreserved(publishedReportText, stagedReportText) {
if (!publishedReportText) {
return;
}
if (!stagedReportText) {
throw new Error("The staged package is missing reports/generation.json");
}

const publishedEntries = readAllowEntryContracts(publishedReportText, "published");
const stagedEntries = readAllowEntryContracts(stagedReportText, "staged");
for (const [entryName, publishedCompatKeys] of publishedEntries) {
const stagedCompatKeys = stagedEntries.get(entryName);
if (!stagedCompatKeys || JSON.stringify(stagedCompatKeys) !== JSON.stringify(publishedCompatKeys)) {
throw new Error(`Published allow entry contract changed: allow/${entryName}`);
}
}
}

/**
* @param {string} reportText
* @param {string} label
*/
function readAllowEntryContracts(reportText, label) {
/** @type {{ allowEntries?: Array<{ entryName?: unknown; compatKeys?: unknown; }>; }} */
const report = JSON.parse(reportText);
const contracts = new Map();
for (const entry of report.allowEntries ?? []) {
if (
typeof entry.entryName !== "string"
|| !Array.isArray(entry.compatKeys)
|| entry.compatKeys.some(compatKey => typeof compatKey !== "string")
|| contracts.has(entry.entryName)
) {
throw new Error(`Invalid ${label} allow entry contract report`);
}
contracts.set(entry.entryName, [...entry.compatKeys].sort(compareStrings));
}
return contracts;
}

/**
* @param {PackageRegistryEntry} packageConfig
*/
Expand Down Expand Up @@ -430,16 +495,18 @@ async function fetchPackageMetadata(packageName) {
* @param {string} directoryPath
*/
export async function createPackageTarball(directoryPath) {
await mkdir(path.join(repoRoot, ".tmp", "release-tarballs"), { recursive: true });
const packOutput = execFileSync("npm", ["pack", directoryPath, "--pack-destination", path.join(repoRoot, ".tmp", "release-tarballs"), "--silent"], {
const tarballRoot = path.join(repoRoot, ".tmp", "release-tarballs");
await mkdir(tarballRoot, { recursive: true });
const tarballDirectory = await mkdtemp(path.join(tarballRoot, "pack-"));
const packOutput = execFileSync("npm", ["pack", directoryPath, "--pack-destination", tarballDirectory, "--silent"], {
cwd: repoRoot,
encoding: "utf8",
}).trim();
const tarballName = packOutput.split(/\r?\n/).filter(Boolean).at(-1);
if (!tarballName) {
throw new Error(`npm pack did not return a tarball name for ${directoryPath}`);
}
return path.join(repoRoot, ".tmp", "release-tarballs", tarballName);
return path.join(tarballDirectory, tarballName);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions deploy/package-registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const baselinePackage = {
from: path.join(repoRoot, "generated", "current", "baseline.d.ts"),
to: "baseline.d.ts",
},
{
from: path.join(repoRoot, "generated", "current", "allow"),
to: "allow",
},
{
from: path.join(repoRoot, "derived", "current", "classification.json"),
to: path.join("reports", "classification.json"),
Expand Down
25 changes: 24 additions & 1 deletion deploy/readmes/baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,31 @@ The same snapshot facts are available to tools through `{{PACKAGE_NAME}}/snapsho

Now only the supported Baseline widely available JavaScript surfaces type-check; APIs that haven't reached Baseline yet are reported as errors.

## Allow a polyfilled feature

When the runtime loads an audited polyfill, add its generated web-features entry after the base package. For example, core-js can provide `Promise.withResolvers` at runtime:

```ts
import "core-js/proposals/promise-with-resolvers";
```

```json
{
"compilerOptions": {
"noLib": true,
"types": [
"{{PACKAGE_NAME}}",
"{{PACKAGE_NAME}}/allow/promise-withresolvers"
]
}
}
```

Only explicitly audited entries are public. Published entry paths remain valid after all of their compat keys become Baseline widely available and move into the base package. Limited availability features with `baselineStatus: false` are rejected.

## Notes

- The public surface is a single `baseline` lib.
- The base public surface is a single `baseline` lib.
- Audited `allow/*` entrypoints are optional additions for explicitly polyfilled APIs.
- The current scope is `javascript.builtins.*` plus the `arguments` object.
- The generated declarations are derived from the npm `typescript` package and preserve the upstream Microsoft license notice inside `baseline.d.ts`.
Loading