diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cf4906d6..42fc5e5c 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -43,3 +43,5 @@ jobs: TEST_PROBE_ONLY: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/2.x' }} BS_USERNAME: ${{ secrets.BS_USERNAME }} BS_ACCESSKEY: ${{ secrets.BS_ACCESSKEY }} + - name: Verify TypeScript + run: npm run verify-typescript diff --git a/package.json b/package.json index ef968139..66eee4a2 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test:jsdom": "cross-env NODE_ENV=test BABEL_ENV=rollup node test/jsdom-node-runner --dot", "test:karma": "cross-env NODE_ENV=test BABEL_ENV=rollup karma start test/karma.conf.js --log-level warn ", "test:ci": "cross-env NODE_ENV=test BABEL_ENV=rollup npm run test:jsdom && npm run test:karma -- --log-level error --reporters dots --single-run --shouldTestOnBrowserStack=\"${TEST_BROWSERSTACK}\" --shouldProbeOnly=\"${TEST_PROBE_ONLY}\"", - "test": "cross-env NODE_ENV=test BABEL_ENV=rollup npm run lint && npm run test:jsdom && npm run test:karma -- --browsers Chrome" + "test": "cross-env NODE_ENV=test BABEL_ENV=rollup npm run lint && npm run test:jsdom && npm run test:karma -- --browsers Chrome", + "verify-typescript": "node ./typescript/verify.js" }, "main": "./dist/purify.cjs.js", "module": "./dist/purify.es.mjs", diff --git a/typescript/commonjs-with-no-types/index.ts b/typescript/commonjs-with-no-types/index.ts new file mode 100644 index 00000000..d7e8356e --- /dev/null +++ b/typescript/commonjs-with-no-types/index.ts @@ -0,0 +1,4 @@ +import * as dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/commonjs-with-no-types/tsconfig.json b/typescript/commonjs-with-no-types/tsconfig.json new file mode 100644 index 00000000..17a8253b --- /dev/null +++ b/typescript/commonjs-with-no-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2015", + "typeRoots": ["types"] + } +} diff --git a/typescript/commonjs-with-no-types/types/readme.md b/typescript/commonjs-with-no-types/types/readme.md new file mode 100644 index 00000000..2ce15c2b --- /dev/null +++ b/typescript/commonjs-with-no-types/types/readme.md @@ -0,0 +1 @@ +This simulates not having `@types/trusted-types` installed. diff --git a/typescript/commonjs-with-specific-types/index.ts b/typescript/commonjs-with-specific-types/index.ts new file mode 100644 index 00000000..d7e8356e --- /dev/null +++ b/typescript/commonjs-with-specific-types/index.ts @@ -0,0 +1,4 @@ +import * as dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/commonjs-with-specific-types/tsconfig.json b/typescript/commonjs-with-specific-types/tsconfig.json new file mode 100644 index 00000000..a3a883d5 --- /dev/null +++ b/typescript/commonjs-with-specific-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2015", + "types": ["node"] + } +} diff --git a/typescript/commonjs/index.ts b/typescript/commonjs/index.ts new file mode 100644 index 00000000..d7e8356e --- /dev/null +++ b/typescript/commonjs/index.ts @@ -0,0 +1,4 @@ +import * as dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/commonjs/tsconfig.json b/typescript/commonjs/tsconfig.json new file mode 100644 index 00000000..0b61b0c7 --- /dev/null +++ b/typescript/commonjs/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2015" + } +} diff --git a/typescript/esm-with-no-types/index.ts b/typescript/esm-with-no-types/index.ts new file mode 100644 index 00000000..88dad05e --- /dev/null +++ b/typescript/esm-with-no-types/index.ts @@ -0,0 +1,4 @@ +import dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/esm-with-no-types/package.json b/typescript/esm-with-no-types/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/typescript/esm-with-no-types/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/typescript/esm-with-no-types/tsconfig.json b/typescript/esm-with-no-types/tsconfig.json new file mode 100644 index 00000000..842f2303 --- /dev/null +++ b/typescript/esm-with-no-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "bundler", + "typeRoots": ["types"] + } +} diff --git a/typescript/esm-with-no-types/types/readme.md b/typescript/esm-with-no-types/types/readme.md new file mode 100644 index 00000000..2ce15c2b --- /dev/null +++ b/typescript/esm-with-no-types/types/readme.md @@ -0,0 +1 @@ +This simulates not having `@types/trusted-types` installed. diff --git a/typescript/esm-with-specific-types/index.ts b/typescript/esm-with-specific-types/index.ts new file mode 100644 index 00000000..88dad05e --- /dev/null +++ b/typescript/esm-with-specific-types/index.ts @@ -0,0 +1,4 @@ +import dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/esm-with-specific-types/package.json b/typescript/esm-with-specific-types/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/typescript/esm-with-specific-types/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/typescript/esm-with-specific-types/tsconfig.json b/typescript/esm-with-specific-types/tsconfig.json new file mode 100644 index 00000000..6453dabe --- /dev/null +++ b/typescript/esm-with-specific-types/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "bundler", + "types": ["node"] + } +} diff --git a/typescript/esm/index.ts b/typescript/esm/index.ts new file mode 100644 index 00000000..88dad05e --- /dev/null +++ b/typescript/esm/index.ts @@ -0,0 +1,4 @@ +import dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/esm/package.json b/typescript/esm/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/typescript/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/typescript/esm/tsconfig.json b/typescript/esm/tsconfig.json new file mode 100644 index 00000000..d2063401 --- /dev/null +++ b/typescript/esm/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/typescript/nodenext/index.ts b/typescript/nodenext/index.ts new file mode 100644 index 00000000..88dad05e --- /dev/null +++ b/typescript/nodenext/index.ts @@ -0,0 +1,4 @@ +import dompurify from 'dompurify'; + +dompurify.sanitize('

'); +dompurify().sanitize('

'); diff --git a/typescript/nodenext/tsconfig.json b/typescript/nodenext/tsconfig.json new file mode 100644 index 00000000..d82f49d3 --- /dev/null +++ b/typescript/nodenext/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "nodenext" + } +} diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 00000000..39e10f84 --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "dompurify": "file:.." + } +} diff --git a/typescript/verify.js b/typescript/verify.js new file mode 100644 index 00000000..032db0c8 --- /dev/null +++ b/typescript/verify.js @@ -0,0 +1,122 @@ +// @ts-check + +const fs = require('node:fs/promises'); +const path = require('node:path'); +const ts = require('typescript'); +const { exec } = require('child_process'); + +run().catch((ex) => { + console.error(ex); + process.exitCode = 1; +}); + +async function run() { + // Install node modules so that `dompurify` can be resolved. + process.stdout.write(`\x1b[30mInstalling node modules...\x1b[0m\n`); + + await new Promise((resolve, reject) => { + exec('npm install --no-package-lock', { cwd: __dirname }, (err) => { + if (err) { + reject(err); + } else { + resolve(undefined); + } + }); + }); + + process.stdout.write('\n'); + + let projects = await fs.readdir(__dirname, { withFileTypes: true }); + + for (let project of projects + .filter((x) => x.isDirectory()) + .filter((x) => x.name !== 'node_modules') + .sort((a, b) => a.name.localeCompare(b.name))) { + await verify(project.name, path.join(__dirname, project.name)); + } +} + +/** + * Verifies that a TypeScript project compiles. + * @param {string} name The name of the project. + * @param {string} directory The project directory. + * @returns {Promise} + */ +async function verify(name, directory) { + let line = ` ${name}...`; + process.stdout.write(line); + + let diagnostics = await compile(path.join(directory, 'tsconfig.json')); + let success = diagnostics.length === 0; + let report = `\x1b${success ? '[32m✔' : '[31mX'}\x1b[0m`; + + if (process.stdout.isTTY) { + process.stdout.write(`\x1b[${line.length}D${report} ${name} \n`); + } else { + process.stdout.write(` ${report}\n`); + } + + if (!success) { + printDiagnostics(diagnostics); + process.exitCode = 1; + } +} + +/** + * Compiles a TypeScript project. + * @param {string} configFileName The file name of the TypeScript config file. + * @returns {Promise} The diagnostics produced. + */ +async function compile(configFileName) { + let jsonParseResult = ts.parseConfigFileTextToJson( + configFileName, + await fs.readFile(configFileName, { encoding: 'utf8' }) + ); + + if (!jsonParseResult.config && jsonParseResult.error) { + return [jsonParseResult.error]; + } + + let config = ts.parseJsonConfigFileContent( + jsonParseResult.config, + ts.sys, + path.dirname(configFileName) + ); + if (config.errors.length > 0) { + return config.errors; + } + + let program = ts.createProgram(config.fileNames, config.options); + let emitResult = program.emit( + undefined, + // Do not emit anything. + () => undefined + ); + + return ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); +} + +/** + * Prints the diagnostics to stdout. + * @param {ts.Diagnostic[]} diagnostics The diagnostics to report. + */ +function printDiagnostics(diagnostics) { + diagnostics.forEach((diagnostic) => { + let message = ''; + + if (diagnostic.file && diagnostic.start) { + let start = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start + ); + + message += ` ${diagnostic.file.fileName} (${start.line + 1},${ + start.character + 1 + })`; + } + + message += + ': ' + ts.flattenDiagnosticMessageText(diagnostic.messageText, ' \n'); + + process.stdout.write(`\x1b[30m ${message}\x1b[0m\n`); + }); +}