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);
}
- }
+ });