markpaste 0.0.2 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +36 -0
- package/README.md +2 -0
- package/bin/markpaste +17 -23
- package/package.json +11 -6
- package/playwright.config.ts +2 -2
- package/scripts/compare-converters.js +119 -0
- package/src/cleaner.js +32 -0
- package/test/node/cleaner.test.js +21 -0
- package/test/web/basic-load.spec.ts +2 -2
- package/test/web/cleaner.spec.ts +1 -1
- package/test/web/pasting.spec.ts +3 -3
- package/TYPESCRIPT.md +0 -109
- package/pnpm-workspace.yaml +0 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
with:
|
|
20
|
+
version: 10.22.0
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: 22.14.0 # Minimum for Trusted Publishing
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
26
|
+
cache: 'pnpm'
|
|
27
|
+
|
|
28
|
+
- run: pnpm install --frozen-lockfile
|
|
29
|
+
- run: pnpm exec playwright install
|
|
30
|
+
|
|
31
|
+
- run: pnpm test
|
|
32
|
+
|
|
33
|
+
- name: Publish to npm
|
|
34
|
+
run: |
|
|
35
|
+
npm install -g npm@latest
|
|
36
|
+
npm publish --provenance --access public --registry https://registry.npmjs.org/
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# MarkPaste
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/markpaste)
|
|
4
|
+
|
|
3
5
|
MarkPaste is an isomorphic tool that converts rich text to Markdown. It works seamlessly in both the **browser** as a web application and in **Node.js** as a library.
|
|
4
6
|
|
|
5
7
|
## Features
|
package/bin/markpaste
CHANGED
|
@@ -21,10 +21,13 @@ import { marked } from 'marked';
|
|
|
21
21
|
/**
|
|
22
22
|
* Formats a string for preview in the logs (truncated, escaped, and dimmed).
|
|
23
23
|
*/
|
|
24
|
-
function textToPreview(text,
|
|
24
|
+
function textToPreview(text, labelWidth = 0) {
|
|
25
25
|
const dimmedStart = '\x1b[2m';
|
|
26
26
|
const dimmedEnd = '\x1b[22m';
|
|
27
|
-
const
|
|
27
|
+
const columns = process.stdout.columns || 80;
|
|
28
|
+
// maxLength = columns - labelWidth - 4 (for " ...") - 1 (safety)
|
|
29
|
+
const maxLength = Math.max(10, columns - labelWidth - 5);
|
|
30
|
+
const preview = text.substring(0, maxLength).replace(/\n/g, '\\n');
|
|
28
31
|
return `${dimmedStart}${preview}${dimmedEnd}...`;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -85,16 +88,17 @@ async function main() {
|
|
|
85
88
|
process.exit(1);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
console.log(`Reading from clipboard... (${input.length} chars)`);
|
|
89
|
-
console.log(` Preview: ${textToPreview(input)}`);
|
|
90
|
-
|
|
91
91
|
const isMarkdown = isProbablyMarkdown(input, hasHtmlFlavor);
|
|
92
92
|
|
|
93
|
+
console.log('Read from clipboard:');
|
|
94
|
+
const typeLabel = isMarkdown ? 'Markdown ' : 'HTML ';
|
|
95
|
+
const inputLabel = `${typeLabel}(${input.length} chars): `;
|
|
96
|
+
console.log(` ${inputLabel}${textToPreview(input, inputLabel.length + 2)}`);
|
|
97
|
+
|
|
93
98
|
let cleanedHtml;
|
|
94
99
|
let markdown;
|
|
95
100
|
|
|
96
101
|
if (isMarkdown) {
|
|
97
|
-
console.log('✨ Markdown detected. Rendering to HTML...');
|
|
98
102
|
markdown = input;
|
|
99
103
|
cleanedHtml = await marked.parse(input);
|
|
100
104
|
} else {
|
|
@@ -102,25 +106,15 @@ async function main() {
|
|
|
102
106
|
markdown = await convert(input, { clean: true, isMarkdown: false });
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
console.log('Updating clipboard...');
|
|
106
109
|
setClipboard(cleanedHtml, markdown);
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// console.warn(' Reason: No HTML flavor found after setting.');
|
|
116
|
-
// } else {
|
|
117
|
-
// console.warn(` Reason: Length difference (${cleanedHtml.length} vs ${verifiedHtml.length})`);
|
|
118
|
-
// }
|
|
119
|
-
// }
|
|
120
|
-
|
|
121
|
-
console.log('\n✅ Clipboard updated with:');
|
|
122
|
-
console.log(` - Plain Text (Markdown): ${textToPreview(markdown)}`);
|
|
123
|
-
console.log(` - HTML (Cleaned): ${textToPreview(cleanedHtml)}`);
|
|
111
|
+
const mdLabel = `Markdown (${markdown.length} chars): `;
|
|
112
|
+
const htmlLabel = `HTML (${cleanedHtml.length} chars): `;
|
|
113
|
+
const labelWidth = Math.max(mdLabel.length, htmlLabel.length) + 2;
|
|
114
|
+
|
|
115
|
+
console.log('\nClipboard updated with:');
|
|
116
|
+
console.log(` ${mdLabel.padEnd(labelWidth - 2)}${textToPreview(markdown, labelWidth)}`);
|
|
117
|
+
console.log(` ${htmlLabel.padEnd(labelWidth - 2)}${textToPreview(cleanedHtml, labelWidth)}`);
|
|
124
118
|
|
|
125
119
|
} catch (err) {
|
|
126
120
|
console.error('Error during conversion:', err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markpaste",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "HTML and Markdown, perfected for pasting into documents, emails, and more. So isomorphic. lol.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
"test:node": "node --test test/node/**/*.test.js"
|
|
16
16
|
},
|
|
17
17
|
"packageManager": "pnpm@10.22.0",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
18
|
+
"keywords": [
|
|
19
|
+
"markdown",
|
|
20
|
+
"html",
|
|
21
|
+
"clipboard",
|
|
22
|
+
"isomorphic"
|
|
23
|
+
],
|
|
24
|
+
"author": "Paul Irish <npm@paul.irish>",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/paulirish/markpaste"
|
|
21
28
|
},
|
|
22
|
-
"keywords": [],
|
|
23
|
-
"author": "",
|
|
24
29
|
"license": "Apache-2.0",
|
|
25
30
|
"type": "module",
|
|
26
31
|
"devDependencies": {
|
package/playwright.config.ts
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { getConverter } from '../src/converter.js';
|
|
8
|
+
import { cleanHTML } from '../src/cleaner.js';
|
|
9
|
+
|
|
10
|
+
const testCases = [
|
|
11
|
+
{
|
|
12
|
+
name: 'Basic text',
|
|
13
|
+
html: '<p>Hello <b>world</b> and <i>everyone</i>!</p>'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'Headings',
|
|
17
|
+
html: '<h1>Main Heading</h1><h2>Sub Heading</h2><h3>Third level</h3>'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Lists',
|
|
21
|
+
html: '<ul><li>Item 1</li><li>Item 2</li></ul><ol><li>First</li><li>Second</li></ol>'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'Links',
|
|
25
|
+
html: '<p>Check out <a href="https://example.com">this link</a>.</p>'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'Code',
|
|
29
|
+
html: '<p>Use <code>console.log()</code> to debug.</p><pre><code>function hello() {\n console.log("world");\n}</code></pre>'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Tables',
|
|
33
|
+
html: `
|
|
34
|
+
<table>
|
|
35
|
+
<thead>
|
|
36
|
+
<tr><th>Col 1</th><th>Col 2</th></tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody>
|
|
39
|
+
<tr><td>Data 1</td><td>Data 2</td></tr>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
`
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Tricky Span',
|
|
46
|
+
html: `
|
|
47
|
+
<p>The<span> </span>
|
|
48
|
+
<code class="w3-codespan">debugger</code>
|
|
49
|
+
<span> </span>keyword stops the execution of JavaScript.</p>
|
|
50
|
+
`
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Nested formatting',
|
|
54
|
+
html: '<p><b>Bold and <i>italic and <u>underlined</u></i></b></p>'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'Images',
|
|
58
|
+
html: '<img src="https://example.com/image.png" alt="An image" title="Image title">'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Blockquotes',
|
|
62
|
+
html: '<blockquote><p>This is a quote.</p><footer>— Someone</footer></blockquote>'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'Horizontal Rule',
|
|
66
|
+
html: '<p>Before</p><hr><p>After</p>'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Task Lists',
|
|
70
|
+
html: '<ul><li>[ ] Todo</li><li>[x] Done</li></ul>'
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
async function runComparison() {
|
|
75
|
+
const turndown = await getConverter('turndown');
|
|
76
|
+
const pandoc = await getConverter('pandoc');
|
|
77
|
+
|
|
78
|
+
console.log('Comparing Turndown vs Pandoc...\n');
|
|
79
|
+
|
|
80
|
+
for (const tc of testCases) {
|
|
81
|
+
console.log(`Test Case: ${tc.name}`);
|
|
82
|
+
|
|
83
|
+
const cleaned = await cleanHTML(tc.html);
|
|
84
|
+
const tOut = (await turndown.convert(cleaned)).trim();
|
|
85
|
+
const pOut = (await pandoc.convert(cleaned)).trim();
|
|
86
|
+
|
|
87
|
+
if (tOut === pOut) {
|
|
88
|
+
console.log(' ✅ Match');
|
|
89
|
+
} else {
|
|
90
|
+
console.log(' ❌ Mismatch - Diffing...');
|
|
91
|
+
|
|
92
|
+
const tmpDir = path.join(os.tmpdir(), 'markpaste-comp');
|
|
93
|
+
if (!existsSync(tmpDir)) mkdirSync(tmpDir);
|
|
94
|
+
|
|
95
|
+
const tFile = path.join(tmpDir, 'turndown.md');
|
|
96
|
+
const pFile = path.join(tmpDir, 'pandoc.md');
|
|
97
|
+
|
|
98
|
+
writeFileSync(tFile, tOut + '\n');
|
|
99
|
+
writeFileSync(pFile, pOut + '\n');
|
|
100
|
+
|
|
101
|
+
// Use git diff --no-index | delta
|
|
102
|
+
const diffCmd = `git --no-pager diff --no-index --color=always ${tFile} ${pFile} | delta`;
|
|
103
|
+
const result = spawnSync('sh', ['-c', diffCmd], { encoding: 'utf8', stdio: 'inherit' });
|
|
104
|
+
|
|
105
|
+
// cleanup
|
|
106
|
+
unlinkSync(tFile);
|
|
107
|
+
unlinkSync(pFile);
|
|
108
|
+
}
|
|
109
|
+
console.log('-'.repeat(40));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cleanup pandoc worker if necessary
|
|
113
|
+
if (pandoc.dispose) pandoc.dispose();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
runComparison().catch(err => {
|
|
117
|
+
console.error(err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
package/src/cleaner.js
CHANGED
|
@@ -106,6 +106,28 @@ function processNode(sourceNode, targetParent) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
if (ALLOWED_TAGS.includes(tagName)) {
|
|
109
|
+
// Special case: B or STRONG with font-weight: normal should be unwrapped
|
|
110
|
+
if (tagName === 'B' || tagName === 'STRONG') {
|
|
111
|
+
const style = sourceNode.getAttribute('style') || '';
|
|
112
|
+
if (/font-weight\s*:\s*(normal|400|lighter)/i.test(style)) {
|
|
113
|
+
Array.from(sourceNode.childNodes).forEach(child => {
|
|
114
|
+
processNode(child, targetParent);
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Special case: I or EM with font-style: normal should be unwrapped
|
|
121
|
+
if (tagName === 'I' || tagName === 'EM') {
|
|
122
|
+
const style = sourceNode.getAttribute('style') || '';
|
|
123
|
+
if (/font-style\s*:\s*normal/i.test(style)) {
|
|
124
|
+
Array.from(sourceNode.childNodes).forEach(child => {
|
|
125
|
+
processNode(child, targetParent);
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
// Special case: UL/OL without LI children (often a bug in clipboard content)
|
|
110
132
|
// This tweak should only happen when this element is the FIRST element in the received DOM.
|
|
111
133
|
if (tagName === 'UL' || tagName === 'OL') {
|
|
@@ -160,6 +182,16 @@ function processNode(sourceNode, targetParent) {
|
|
|
160
182
|
export function removeStyleAttributes(html) {
|
|
161
183
|
const doc = parseHTMLGlobal(html);
|
|
162
184
|
const body = doc.body;
|
|
185
|
+
|
|
186
|
+
// Strip dangerous tags even when "Clean HTML" is off
|
|
187
|
+
const DANGEROUS_TAGS = ['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'LINK', 'META'];
|
|
188
|
+
DANGEROUS_TAGS.forEach(tag => {
|
|
189
|
+
const elements = body.querySelectorAll(tag);
|
|
190
|
+
for (let i = 0; i < elements.length; i++) {
|
|
191
|
+
elements[i].remove();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
163
195
|
const allElements = body.querySelectorAll('*');
|
|
164
196
|
for (let i = 0; i < allElements.length; i++) {
|
|
165
197
|
allElements[i].removeAttribute('style');
|
|
@@ -46,3 +46,24 @@ test('cleaner: handles leading OL/UL tags correctly', async () => {
|
|
|
46
46
|
assert.strictEqual(cleaned.toLowerCase().includes('<h1'), true);
|
|
47
47
|
assert.strictEqual(cleaned.toLowerCase().includes('manage python packages'), true);
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
test('cleaner: should unwrap <b style="font-weight: normal"> and equivalents', async () => {
|
|
51
|
+
const cases = [
|
|
52
|
+
['<b style="font-weight: normal">Not bold</b>', 'Not bold'],
|
|
53
|
+
['<strong style="font-weight: normal">Not bold</strong>', 'Not bold'],
|
|
54
|
+
['<b style="font-weight: 400">Not bold</b>', 'Not bold'],
|
|
55
|
+
['<b style="FONT-WEIGHT: NORMAL">Not bold</b>', 'Not bold'],
|
|
56
|
+
['<b style="font-weight: lighter">Not bold</b>', 'Not bold'],
|
|
57
|
+
['<i style="font-style: normal">Not italic</i>', 'Not italic'],
|
|
58
|
+
['<em style="font-style: normal">Not italic</em>', 'Not italic'],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const [input, expected] of cases) {
|
|
62
|
+
const cleaned = await cleanHTML(input);
|
|
63
|
+
assert.strictEqual(cleaned, expected, `Failed for input: ${input}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Should keep normal <b>
|
|
67
|
+
const bold = await cleanHTML('<b>Bold</b>');
|
|
68
|
+
assert.ok(bold.toUpperCase().includes('<B>BOLD</B>'));
|
|
69
|
+
});
|
|
@@ -27,7 +27,7 @@ test('should load the page without errors or 404s', async ({page}) => {
|
|
|
27
27
|
consoleErrors.push(error.message);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
await page.goto('http://127.0.0.1:
|
|
30
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
31
31
|
|
|
32
32
|
// Verify no network failures
|
|
33
33
|
expect(failedRequests, `Found failed network requests: ${failedRequests.join(', ')}`).toHaveLength(0);
|
|
@@ -40,7 +40,7 @@ test('should load the page without errors or 404s', async ({page}) => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('should paste HTML and update all 2 markdown outputs', async ({page}) => {
|
|
43
|
-
await page.goto('http://127.0.0.1:
|
|
43
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
44
44
|
|
|
45
45
|
const inputArea = page.locator('#inputArea');
|
|
46
46
|
const testHtml = '<h1>Test Title</h1><p>Hello <strong>world</strong></p>';
|
package/test/web/cleaner.spec.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {test, expect} from '@playwright/test';
|
|
|
2
2
|
|
|
3
3
|
test.describe('Cleaner functionality', () => {
|
|
4
4
|
test.beforeEach(async ({page}) => {
|
|
5
|
-
await page.goto('http://127.0.0.1:
|
|
5
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
test('should remove MDN copy button and play links', async ({page}) => {
|
package/test/web/pasting.spec.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {test, expect} from '@playwright/test';
|
|
|
2
2
|
|
|
3
3
|
test.describe('MarkPaste functionality', () => {
|
|
4
4
|
test.beforeEach(async ({page}) => {
|
|
5
|
-
await page.goto('http://127.0.0.1:
|
|
5
|
+
await page.goto('http://127.0.0.1:7025/index.html');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
test('should convert basic rich text to markdown', async ({page}) => {
|
|
@@ -56,8 +56,7 @@ test.describe('MarkPaste functionality', () => {
|
|
|
56
56
|
await expect(pandocOutput).toContainText('Hello World');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
test.skip('should toggle HTML cleaning', async ({page}) => {
|
|
59
|
+
test('should toggle HTML cleaning', async ({page}) => {
|
|
61
60
|
const html = '<div><p>Hello</p><style>body{color:red;}</style><script>alert("xss")</script></div>';
|
|
62
61
|
|
|
63
62
|
await page.evaluate(html => {
|
|
@@ -81,6 +80,7 @@ test.describe('MarkPaste functionality', () => {
|
|
|
81
80
|
|
|
82
81
|
expect(await htmlCode.textContent()).toContain('<div>');
|
|
83
82
|
expect(await htmlCode.textContent()).not.toContain('<script>');
|
|
83
|
+
expect(await htmlCode.textContent()).not.toContain('<style>');
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
test('should retain table structure', async ({page}) => {
|
package/TYPESCRIPT.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# Guide: Modern Type Checking without a Build Step
|
|
2
|
-
|
|
3
|
-
This guide outlines a setup for achieving type safety in both browser and Node.js environments while avoiding a compilation/build step.
|
|
4
|
-
|
|
5
|
-
## Core Philosophy
|
|
6
|
-
|
|
7
|
-
1. **Browser (Client-side)**: Use pure JavaScript with **JSDoc annotations**. This ensures the code runs directly in the browser without bundling or transpilation.
|
|
8
|
-
2. **Node.js (Server-side/Tooling)**: Use **TypeScript with Erasable Syntax**. This allows Node.js to execute `.ts` files natively while benefiting from TypeScript's static analysis.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 1. Browser-side: JavaScript + JSDoc
|
|
13
|
-
|
|
14
|
-
Maintain all frontend logic in `.js` files. Use JSDoc to define types, interfaces, and function signatures.
|
|
15
|
-
|
|
16
|
-
### Patterns
|
|
17
|
-
|
|
18
|
-
- Use `@type` for variable declarations.
|
|
19
|
-
- Use `@param` and `@returns` for functions.
|
|
20
|
-
- Use `@typedef` or `@template` for complex types.
|
|
21
|
-
- Import types from other files using `import('./path/to/file.js').TypeName`.
|
|
22
|
-
|
|
23
|
-
```javascript
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} input
|
|
26
|
-
* @param {Object} options
|
|
27
|
-
* @param {boolean} [options.verbose]
|
|
28
|
-
* @returns {Promise<string>}
|
|
29
|
-
*/
|
|
30
|
-
export async function processData(input, {verbose = false} = {}) {
|
|
31
|
-
// ...
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 2. Node.js: TypeScript + Erasable Syntax
|
|
38
|
-
|
|
39
|
-
Use `.ts` files for Node.js logic. Stick to **Erasable Syntax Only**—features that can be removed by simply stripping the type annotations.
|
|
40
|
-
|
|
41
|
-
### Prohibited Features (Non-Erasable)
|
|
42
|
-
|
|
43
|
-
- `enum`
|
|
44
|
-
- `namespaces`
|
|
45
|
-
- `parameter properties` in constructors (e.g., `constructor(public name: string) {}`)
|
|
46
|
-
- `experimentalDecorators`
|
|
47
|
-
|
|
48
|
-
### Required Conventions
|
|
49
|
-
|
|
50
|
-
- **Explicit Extensions**: Always use the `.ts` extension in import statements.
|
|
51
|
-
- **Explicit Type Imports**: Always use `import type` for types to ensure they are safely erasable.
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
import {helper} from './utils.ts';
|
|
55
|
-
import type {Config} from './types.ts';
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 3. Configuration Requirements
|
|
61
|
-
|
|
62
|
-
### `tsconfig.json`
|
|
63
|
-
|
|
64
|
-
The configuration must support both JSDoc checking and native TypeScript execution.
|
|
65
|
-
|
|
66
|
-
```json
|
|
67
|
-
{
|
|
68
|
-
"compilerOptions": {
|
|
69
|
-
"target": "esnext",
|
|
70
|
-
"module": "nodenext",
|
|
71
|
-
/* Native TS Execution Flags */
|
|
72
|
-
"erasableSyntaxOnly": true,
|
|
73
|
-
"verbatimModuleSyntax": true,
|
|
74
|
-
"allowImportingTsExtensions": true,
|
|
75
|
-
"rewriteRelativeImportExtensions": true,
|
|
76
|
-
|
|
77
|
-
/* Type Checking Strategy */
|
|
78
|
-
"noEmit": true,
|
|
79
|
-
"allowJs": true,
|
|
80
|
-
"checkJs": true,
|
|
81
|
-
"skipLibCheck": true,
|
|
82
|
-
"strict": true
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### `package.json`
|
|
88
|
-
|
|
89
|
-
The project must be an ES Module.
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"type": "module",
|
|
94
|
-
"engines": {
|
|
95
|
-
"node": ">=24.11.0"
|
|
96
|
-
},
|
|
97
|
-
"scripts": {
|
|
98
|
-
"typecheck": "tsc --noEmit"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 4. Environment & Tooling
|
|
106
|
-
|
|
107
|
-
- **Node.js**: Version **v24.11.0+** is required for stable native TypeScript support.
|
|
108
|
-
- **IDE**: VS Code (or any LSP-compliant editor) will automatically pick up the `tsconfig.json` to provide real-time feedback for both `.js` (via JSDoc) and `.ts` files.
|
|
109
|
-
- **CI/CD**: Run `npm run typecheck` (or equivalent) as a blocking step in your pipeline.
|
package/pnpm-workspace.yaml
DELETED