diff --git a/.github/workflows/esm-lint.yml b/.github/workflows/esm-lint.yml deleted file mode 100644 index fc82114a..00000000 --- a/.github/workflows/esm-lint.yml +++ /dev/null @@ -1,133 +0,0 @@ -env: - IMPORT_STATEMENT: export * as pageDetect from "github-url-detection" - -# FILE GENERATED WITH: npx ghat fregante/ghatemplates/esm-lint -# SOURCE: https://github.com/fregante/ghatemplates -# OPTIONS: {"exclude":["jobs.Snowpack"]} - -name: ESM -on: - pull_request: - branches: - - '*' - push: - branches: - - master - - main -jobs: - Pack: - 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 - - run: npm pack | tail -1 | xargs -n1 tar -xzf - - uses: actions/upload-artifact@v6 - with: - name: package - path: package - Publint: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: npx publint ./artifact - Webpack: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: npm install --omit=dev ./artifact - - run: echo "$IMPORT_STATEMENT" > index.js - - run: webpack --entry ./index.js - - run: cat dist/main.js - Parcel: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: npm install --omit=dev ./artifact - - run: echo "$IMPORT_STATEMENT" > index.js - - run: > - echo '{"@parcel/resolver-default": {"packageExports": true}}' > - package.json - - run: npx parcel@2 build index.js - - run: cat dist/index.js - Rollup: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: npm install --omit=dev ./artifact rollup@4 @rollup/plugin-json @rollup/plugin-node-resolve - - run: echo "$IMPORT_STATEMENT" > index.js - - run: npx rollup -p node-resolve -p @rollup/plugin-json index.js - Vite: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: npm install --omit=dev ./artifact - - run: echo '' > index.html - - run: npx vite build - - run: cat dist/assets/* - esbuild: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: echo '{}' > package.json - - run: echo "$IMPORT_STATEMENT" > index.js - - run: npm install --omit=dev ./artifact - - run: npx esbuild --bundle index.js - TypeScript: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - run: echo '{"type":"module"}' > package.json - - run: npm install --omit=dev ./artifact @sindresorhus/tsconfig - - run: echo "$IMPORT_STATEMENT" > index.ts - - run: > - echo '{"extends":"@sindresorhus/tsconfig","files":["index.ts"]}' > - tsconfig.json - - run: npx --package typescript -- tsc - - run: cat distribution/index.js - Node: - runs-on: ubuntu-latest - needs: Pack - steps: - - uses: actions/download-artifact@v7 - with: - name: package - path: artifact - - uses: actions/setup-node@v6 - with: - node-version-file: artifact/package.json - - run: echo "$IMPORT_STATEMENT" > index.mjs - - run: npm install --omit=dev ./artifact - - run: node index.mjs diff --git a/add-examples-to-dts.ts b/add-examples-to-dts.ts index 4c03c060..42c98fe7 100644 --- a/add-examples-to-dts.ts +++ b/add-examples-to-dts.ts @@ -1,6 +1,7 @@ -/* eslint-disable n/prefer-global/process, unicorn/no-process-exit */ +/* eslint-disable n/prefer-global/process, unicorn/no-process-exit, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument */ import {readFileSync, writeFileSync} from 'node:fs'; import {execSync} from 'node:child_process'; +import {Project, type JSDocableNode} from 'ts-morph'; // Import index.ts to populate the test data via side effect // eslint-disable-next-line import-x/no-unassigned-import import './index.ts'; @@ -17,16 +18,64 @@ if (dtsContent.includes(marker)) { process.exit(1); } -// Process each exported function -const lines = dtsContent.split('\n'); -const outputLines: string[] = []; +// Create a ts-morph project and load the file +const project = new Project(); +const sourceFile = project.createSourceFile(dtsPath, dtsContent, {overwrite: true}); + 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]; +/** + * Add example URLs to a JSDocable node (e.g., variable statement or type alias) + */ +function addExamplesToNode(node: JSDocableNode, urlExamples: string[]): void { + const jsDoc = node.getJsDocs().at(0); + + if (jsDoc) { + // Add @example tags to existing JSDoc + const existingTags = jsDoc.getTags(); + const description = jsDoc.getDescription().trim(); + + // Build new JSDoc content + const newJsDocLines: string[] = []; + if (description) { + newJsDocLines.push(description); + } + + // Add existing tags (that aren't @example tags) + for (const tag of existingTags) { + if (tag.getTagName() !== 'example') { + newJsDocLines.push(tag.getText()); + } + } + + // Add new @example tags + for (const url of urlExamples) { + newJsDocLines.push(`@example ${url}`); + } + + // Replace the JSDoc + jsDoc.remove(); + node.addJsDoc(newJsDocLines.join('\n')); + } else { + // Create new JSDoc with examples + const jsDocLines: string[] = []; + for (const url of urlExamples) { + jsDocLines.push(`@example ${url}`); + } + + node.addJsDoc(jsDocLines.join('\n')); + } +} + +// Process each exported variable declaration (these are the function declarations) +for (const statement of sourceFile.getVariableStatements()) { + // Only process exported statements + if (!statement.isExported()) { + continue; + } + + for (const declaration of statement.getDeclarations()) { + const functionName = declaration.getName(); // Get the tests/examples for this function const examples = getTests(functionName); @@ -37,95 +86,33 @@ for (const line of lines) { 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; - } + addExamplesToNode(statement, urlExamples); + examplesAdded += urlExamples.length; } } } - - outputLines.push(line); } -// Add marker at the beginning -const finalContent = `${marker}\n${outputLines.join('\n')}`; +// Also process exported type aliases (like RepoExplorerInfo) +for (const typeAlias of sourceFile.getTypeAliases()) { + if (!typeAlias.isExported()) { + continue; + } + + const typeName = typeAlias.getName(); + + // Get the tests/examples for this type (unlikely but keeping consistency) + const examples = getTests(typeName); + + if (examples && examples.length > 0 && examples[0] !== 'combinedTestOnly') { + const urlExamples = examples.filter((url: string) => url.startsWith('http')); + + if (urlExamples.length > 0) { + addExamplesToNode(typeAlias, urlExamples); + examplesAdded += urlExamples.length; + } + } +} // Validate that we added some examples if (examplesAdded === 0) { @@ -133,6 +120,10 @@ if (examplesAdded === 0) { process.exit(1); } +// Get the modified content and add marker +const modifiedContent = sourceFile.getFullText(); +const finalContent = `${marker}\n${modifiedContent}`; + // Write the modified content back writeFileSync(dtsPath, finalContent, 'utf8'); diff --git a/demo/Index.svelte b/demo/Index.svelte index 4a5e5495..eb0f8dd6 100644 --- a/demo/Index.svelte +++ b/demo/Index.svelte @@ -7,55 +7,56 @@ const urlParameter = new URLSearchParams(location.search); const parsedUrlParameter = parseUrl(urlParameter.get('url'), 'https://github.com').href; // Parse partial URL in the parameter so that it's shown as full URL in the field - let urlField = parsedUrlParameter || ''; + let urlField = $state(parsedUrlParameter || ''); const allUrls = [...getAllUrls()].sort(); - let parsedUrl; // Do not use ?? because it should work on empty strings - $: parsedUrl = parseUrl(urlField || defaultUrl); + let parsedUrl = $derived(parseUrl(urlField || defaultUrl)); - let detections = []; - $: { - if (parsedUrl) { - if (urlField) { - urlParameter.set('url', urlField.replace('https://github.com', '')); - history.replaceState(null, '', `?${urlParameter}`); - } else { - history.replaceState(null, '', location.pathname); - } - detections = Object.entries(urlDetection) - .map(([name, detect]) => { - if (typeof detect !== 'function') { - return; - } + let detections = $derived( + parsedUrl ? Object.entries(urlDetection) + .map(([name, detect]) => { + if (typeof detect !== 'function') { + return; + } - if (!String(detect).startsWith('()')) { - return { - name, - detect, - result: detect(parsedUrl) - }; - } else { - return {name}; - } - }) - .filter(Boolean) - .sort((a, b) => { - // Pull true values to the top - if (a.result || b.result) { - return a.result ? b.result ? 0 : -1 : 1; - } + if (!String(detect).startsWith('()')) { + return { + name, + detect, + result: detect(parsedUrl) + }; + } else { + return {name}; + } + }) + .filter(Boolean) + .sort((a, b) => { + // Pull true values to the top + if (a.result || b.result) { + return a.result ? b.result ? 0 : -1 : 1; + } - // Push false values to the top - if (a.detect || b.detect) { - return a.detect ? b.detect ? 0 : 1 : -1; - } + // Push false values to the top + if (a.detect || b.detect) { + return a.detect ? b.detect ? 0 : 1 : -1; + } - // DOM-based detections should be in the middle - }); + // DOM-based detections should be in the middle + }) : [] + ); + + $effect(() => { + if (!parsedUrl) return; + + if (urlField) { + urlParameter.set('url', urlField.replace('https://github.com', '')); + history.replaceState(null, '', `?${urlParameter}`); + } else { + history.replaceState(null, '', location.pathname); } - } + });