diff --git a/.github/workflows/esm-lint.yml b/.github/workflows/esm-lint.yml index c6ee3154..fc82114a 100644 --- a/.github/workflows/esm-lint.yml +++ b/.github/workflows/esm-lint.yml @@ -19,6 +19,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: package.json - run: npm install - run: npm run build --if-present - run: npm pack --dry-run diff --git a/add-examples-to-dts.ts b/add-examples-to-dts.ts new file mode 100644 index 00000000..4c03c060 --- /dev/null +++ b/add-examples-to-dts.ts @@ -0,0 +1,153 @@ +/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */ +import {readFileSync, writeFileSync} from 'node:fs'; +import {execSync} from 'node:child_process'; +// Import index.ts to populate the test data via side effect +// eslint-disable-next-line import-x/no-unassigned-import +import './index.ts'; +import {getTests} from './collector.ts'; + +// Read the generated .d.ts file +const dtsPath = './distribution/index.d.ts'; +const dtsContent = readFileSync(dtsPath, 'utf8'); + +// Check if script has already been run +const marker = '/* Examples added by add-examples-to-dts.ts */'; +if (dtsContent.includes(marker)) { + console.error('❌ Error: Examples have already been added to this file'); + process.exit(1); +} + +// Process each exported function +const lines = dtsContent.split('\n'); +const outputLines: string[] = []; +let examplesAdded = 0; + +for (const line of lines) { + // Check if this is a function declaration + const match = /^export declare const (\w+):/.exec(line); + if (match) { + const functionName = match[1]; + + // Get the tests/examples for this function + const examples = getTests(functionName); + + // Only add examples if they exist and aren't the special 'combinedTestOnly' marker + if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') { + // Filter to only include actual URLs (not references to other functions) + const urlExamples = examples.filter((url: string) => url.startsWith('http')); + + if (urlExamples.length > 0) { + // Check if there's an existing JSDoc block immediately before this line + let jsDocumentEndIndex = -1; + let jsDocumentStartIndex = -1; + let isSingleLineJsDocument = false; + + // Look backwards from outputLines to find JSDoc + for (let index = outputLines.length - 1; index >= 0; index--) { + const previousLine = outputLines[index]; + const trimmed = previousLine.trim(); + + if (trimmed === '') { + continue; // Skip empty lines + } + + // Check for single-line JSDoc: /** ... */ + if (trimmed.startsWith('/**') && trimmed.endsWith('*/') && trimmed.length > 5) { + jsDocumentStartIndex = index; + jsDocumentEndIndex = index; + isSingleLineJsDocument = true; + break; + } + + // Check for multi-line JSDoc ending + if (trimmed === '*/') { + jsDocumentEndIndex = index; + // Now find the start of this JSDoc + for (let k = index - 1; k >= 0; k--) { + if (outputLines[k].trim().startsWith('/**')) { + jsDocumentStartIndex = k; + break; + } + } + + break; + } + + // If we hit a non-JSDoc line, there's no JSDoc block + break; + } + + if (jsDocumentStartIndex >= 0 && jsDocumentEndIndex >= 0) { + // Extend existing JSDoc block + if (isSingleLineJsDocument) { + // Convert single-line to multi-line and add examples + const singleLineContent = outputLines[jsDocumentStartIndex]; + // Extract the comment text without /** and */ + const commentText = singleLineContent.trim().slice(3, -2).trim(); + + // Replace the single line with multi-line format + outputLines[jsDocumentStartIndex] = '/**'; + if (commentText) { + outputLines.splice(jsDocumentStartIndex + 1, 0, ` * ${commentText}`); + } + + // Add examples after the existing content + const insertIndex = jsDocumentStartIndex + (commentText ? 2 : 1); + for (const url of urlExamples) { + outputLines.splice(insertIndex + urlExamples.indexOf(url), 0, ` * @example ${url}`); + } + + outputLines.splice(insertIndex + urlExamples.length, 0, ' */'); + examplesAdded += urlExamples.length; + } else { + // Insert @example lines before the closing */ + for (const url of urlExamples) { + outputLines.splice(jsDocumentEndIndex, 0, ` * @example ${url}`); + } + + examplesAdded += urlExamples.length; + } + } else { + // Add new JSDoc comment with examples before the declaration + outputLines.push('/**'); + for (const url of urlExamples) { + outputLines.push(` * @example ${url}`); + } + + outputLines.push(' */'); + examplesAdded += urlExamples.length; + } + } + } + } + + outputLines.push(line); +} + +// Add marker at the beginning +const finalContent = `${marker}\n${outputLines.join('\n')}`; + +// Validate that we added some examples +if (examplesAdded === 0) { + console.error('❌ Error: No examples were added. This likely indicates a problem with the script.'); + process.exit(1); +} + +// Write the modified content back +writeFileSync(dtsPath, finalContent, 'utf8'); + +console.log(`✓ Added ${examplesAdded} example URLs to index.d.ts`); + +// Validate with TypeScript +try { + execSync('npx tsc --noEmit distribution/index.d.ts', { + cwd: process.cwd(), + stdio: 'pipe', + }); + console.log('✓ TypeScript validation passed'); +} catch (error: unknown) { + console.error('❌ TypeScript validation failed:'); + const execError = error as {stdout?: Uint8Array; stderr?: Uint8Array; message?: string}; + console.error(execError.stdout?.toString() ?? execError.stderr?.toString() ?? execError.message); + process.exit(1); +} diff --git a/index.ts b/index.ts index 7e688a9e..18bfef0a 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,5 @@ import reservedNames from 'github-reserved-names/reserved-names.json' with {type: 'json'}; -import {addTests} from './collector.js'; +import {addTests} from './collector.ts'; const $ = (selector: string) => document.querySelector(selector); const exists = (selector: string) => Boolean($(selector)); diff --git a/package-lock.json b/package-lock.json index 7cd8b844..2ab4f431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "devDependencies": { "@sindresorhus/tsconfig": "^8.1.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/node": "^25.0.8", "esbuild": "^0.27.2", "globals": "^17.0.0", "npm-run-all": "^4.1.5", @@ -1439,13 +1440,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.6.tgz", - "integrity": "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==", + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -8189,9 +8188,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", diff --git a/package.json b/package.json index dbc45219..65067444 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "scripts": { "build": "run-p build:*", "build:esbuild": "esbuild index.ts --bundle --external:github-reserved-names --outdir=distribution --format=esm --drop-labels=TEST", - "build:typescript": "tsc --declaration --emitDeclarationOnly", + "build:typescript": "tsc", + "postbuild:typescript": "node add-examples-to-dts.ts", "build:demo": "vite build demo", "try": "esbuild index.ts --bundle --global-name=x --format=iife | pbcopy && echo 'Copied to clipboard'", "fix": "xo --fix", @@ -46,6 +47,7 @@ "devDependencies": { "@sindresorhus/tsconfig": "^8.1.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/node": "^25.0.8", "esbuild": "^0.27.2", "globals": "^17.0.0", "npm-run-all": "^4.1.5", diff --git a/tsconfig.json b/tsconfig.json index abfa2ba1..5a0a8aee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,8 @@ { "extends": "@sindresorhus/tsconfig", "compilerOptions": { - // TODO: Drop after https://github.com/sindresorhus/tsconfig/issues/29 - "resolveJsonModule": true, - "moduleResolution": "Node", - "module": "Preserve" + "emitDeclarationOnly": true, + "allowImportingTsExtensions": true }, "include": [ "index.ts",