diff --git a/.distignore b/.distignore index 5c2347333e7f..ad387e0c4543 100644 --- a/.distignore +++ b/.distignore @@ -69,5 +69,7 @@ README.md rollup.config.js scoper.inc.php SECURITY.md +tsconfig.json +tsconfig.shared.json webpack.config.cjs webpack.config.test.cjs diff --git a/.eslintrc b/.eslintrc index d9b077903788..1dac7ff6eca5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -247,7 +247,8 @@ "^@googleforcreators\\/(.*)\\/(.*)": "./packages/$1/src/$2", "^@googleforcreators\\/(.*)": "./packages/$1/src/", "^@web-stories-wp\\/(.*)": "./packages/$1/src/" - } + }, + "extensions": [ ".js", ".jsx", ".ts", ".tsx" ] } }, "jsdoc": { @@ -281,6 +282,31 @@ ] }, "overrides": [ + { + "files": [ + "**/*.ts", + "**/*.tsx" + ], + "excludedFiles": [ + "**/*.d.ts" + ], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended" + ], + "plugins": [ + "@typescript-eslint" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "no-duplicate-imports": "off", + "@typescript-eslint/no-duplicate-imports": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns-type": "off", + "no-unused-vars": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error" + } + }, { "files": [ "__mocks__/**/*.js", diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7fad589cb9b4..3ebf9b44a325 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,6 +6,7 @@ on: paths: - '**.js' - '**.cjs' + - '**.ts' branches: - main - release/* @@ -14,6 +15,7 @@ on: paths: - '**.js' - '**.cjs' + - '**.ts' # The branches below must be a subset of the branches above branches: - main diff --git a/.github/workflows/lint-css-js-md.yml b/.github/workflows/lint-css-js-md.yml index 29cebd9ebcaf..4811f836b17c 100644 --- a/.github/workflows/lint-css-js-md.yml +++ b/.github/workflows/lint-css-js-md.yml @@ -6,6 +6,7 @@ on: paths: - '**.js' - '**.cjs' + - '**.ts' - '**.css' - 'docs/**/*.md' - 'packages/**/*.md' @@ -25,6 +26,7 @@ on: paths: - '**.js' - '**.cjs' + - '**.ts' - '**.css' - 'docs/**/*.md' - 'packages/**/*.md' diff --git a/.github/workflows/lint-i18n.yml b/.github/workflows/lint-i18n.yml index f1e092c20b2a..13cd011958d9 100644 --- a/.github/workflows/lint-i18n.yml +++ b/.github/workflows/lint-i18n.yml @@ -4,6 +4,8 @@ on: push: paths: - '**.js' + - '**.cjs' + - '**.ts' - '**/package.json' - 'package-lock.json' - 'web-stories.php' @@ -14,6 +16,8 @@ on: pull_request: paths: - '**.js' + - '**.cjs' + - '**.ts' - '**/package.json' - 'package-lock.json' - 'web-stories.php' diff --git a/.github/workflows/tests-karma-dashboard.yml b/.github/workflows/tests-karma-dashboard.yml index c819f381905c..d98fde90f0bb 100644 --- a/.github/workflows/tests-karma-dashboard.yml +++ b/.github/workflows/tests-karma-dashboard.yml @@ -9,6 +9,7 @@ on: - 'webpack.config.cjs' - 'webpack.config.test.cjs' - 'packages/**/*.js' + - 'packages/**/*.ts' - '**/package.json' - 'package-lock.json' - '__static__/**' @@ -23,6 +24,7 @@ on: - 'webpack.config.cjs' - 'webpack.config.test.cjs' - 'packages/**/*.js' + - 'packages/**/*.ts' - '**/package.json' - 'package-lock.json' - '__static__/**' diff --git a/.github/workflows/tests-karma-editor.yml b/.github/workflows/tests-karma-editor.yml index 8923b46a7c23..af3099d5c1eb 100644 --- a/.github/workflows/tests-karma-editor.yml +++ b/.github/workflows/tests-karma-editor.yml @@ -10,6 +10,7 @@ on: - 'webpack.config.test.cjs' - 'packages/**/*.js' - 'packages/**/*.cjs' + - 'packages/**/*.ts' - '**/package.json' - 'package-lock.json' - '__static__/**' @@ -26,6 +27,7 @@ on: - 'webpack.config.test.cjs' - 'packages/**/*.js' - 'packages/**/*.cjs' + - 'packages/**/*.ts' - '**/package.json' - 'package-lock.json' - '__static__/**' diff --git a/.github/workflows/tests-unit-js.yml b/.github/workflows/tests-unit-js.yml index a2f94c43783c..8a0daa73c9fb 100644 --- a/.github/workflows/tests-unit-js.yml +++ b/.github/workflows/tests-unit-js.yml @@ -5,6 +5,8 @@ on: # Only run if JS-related files changed. paths: - '**.js' + - '**.cjs' + - '**.ts' - '**/package.json' - 'package-lock.json' - 'packages/templates/src/raw/**' @@ -18,6 +20,8 @@ on: # Only run if JS-related files changed. paths: - '**.js' + - '**.cjs' + - '**.ts' - '**/package.json' - 'package-lock.json' - 'packages/templates/src/raw/**' diff --git a/.gitignore b/.gitignore index 98dc83451ac1..a77959890bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules /packages/migration/scripts/module.js /packages/*/dist /packages/*/dist-module +/packages/*/dist-types /build /bin/build /bin/local-env/data @@ -22,3 +23,4 @@ phpcs.xml phpunit.xml .phpunit.result.cache /packages/*/package-lock.json +tsconfig.tsbuildinfo diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json index 15f476789b98..be8c3cb00f34 100644 --- a/.npmpackagejsonlintrc.json +++ b/.npmpackagejsonlintrc.json @@ -57,6 +57,7 @@ "exports", "main", "module", + "types", "source", "publishConfig", "sideEffects", diff --git a/babel.config.cjs b/babel.config.cjs index 284735816330..71cf56e99c9c 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -43,6 +43,7 @@ module.exports = function (api) { development: !isProduction, }, ], + '@babel/preset-typescript', ], plugins: [ '@wordpress/babel-plugin-import-jsx-pragma', diff --git a/package-lock.json b/package-lock.json index 7a1f708c0a24..c4fddf192461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@babel/eslint-plugin": "^7.17.7", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", "@googleforcreators/dashboard": "*", "@googleforcreators/design-system": "*", "@googleforcreators/fonts": "*", @@ -30,6 +31,7 @@ "@rollup/plugin-dynamic-import-vars": "^1.4.2", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.2.1", + "@rollup/plugin-typescript": "^8.3.1", "@rollup/plugin-url": "^6.1.0", "@storybook/addon-a11y": "^6.4.20", "@storybook/addon-backgrounds": "^6.4.20", @@ -52,6 +54,7 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.1.0", + "@typescript-eslint/parser": "^5.19.0", "@web-stories-wp/e2e-tests": "*", "@web-stories-wp/eslint-import-resolver": "*", "@web-stories-wp/jest-amp": "*", @@ -144,6 +147,7 @@ "stylelint-processor-styled-components": "^1.10.0", "stylis-plugin-rtl": "^1.0.0", "terser-webpack-plugin": "^5.1.4", + "typescript": "^4.6.3", "webpack": "^5.71.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -5495,6 +5499,24 @@ "rollup": "^2.42.0" } }, + "node_modules/@rollup/plugin-typescript": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.2.tgz", + "integrity": "sha512-MtgyR5LNHZr3GyN0tM7gNO9D0CS+Y+vflS4v/PHmrX17JCkHUYKvQ5jN5o3cz1YKllM3duXUqu3yOHwMPUxhDg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0", + "tslib": "*", + "typescript": ">=3.7.0" + } + }, "node_modules/@rollup/plugin-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-url/-/plugin-url-6.1.0.tgz", @@ -14228,14 +14250,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.2.tgz", - "integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.20.0.tgz", + "integrity": "sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.10.2", - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/typescript-estree": "5.10.2", + "@typescript-eslint/scope-manager": "5.20.0", + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/typescript-estree": "5.20.0", "debug": "^4.3.2" }, "engines": { @@ -14254,6 +14276,104 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", + "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/visitor-keys": "5.20.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", + "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz", + "integrity": "sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/visitor-keys": "5.20.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", + "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.20.0", + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.10.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", @@ -43064,7 +43184,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -50804,6 +50923,16 @@ "resolve": "^1.19.0" } }, + "@rollup/plugin-typescript": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.2.tgz", + "integrity": "sha512-MtgyR5LNHZr3GyN0tM7gNO9D0CS+Y+vflS4v/PHmrX17JCkHUYKvQ5jN5o3cz1YKllM3duXUqu3yOHwMPUxhDg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + } + }, "@rollup/plugin-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-url/-/plugin-url-6.1.0.tgz", @@ -57533,15 +57662,73 @@ } }, "@typescript-eslint/parser": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.2.tgz", - "integrity": "sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.20.0.tgz", + "integrity": "sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.10.2", - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/typescript-estree": "5.10.2", + "@typescript-eslint/scope-manager": "5.20.0", + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/typescript-estree": "5.20.0", "debug": "^4.3.2" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", + "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/visitor-keys": "5.20.0" + } + }, + "@typescript-eslint/types": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", + "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz", + "integrity": "sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/visitor-keys": "5.20.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", + "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.20.0", + "eslint-visitor-keys": "^3.0.0" + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/scope-manager": { @@ -79843,8 +80030,7 @@ "version": "4.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", - "dev": true, - "peer": true + "dev": true }, "ua-parser-js": { "version": "0.7.31", diff --git a/package.json b/package.json index 9ebe808edfb0..80133ef151a4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@babel/eslint-plugin": "^7.17.7", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", "@googleforcreators/dashboard": "*", "@googleforcreators/design-system": "*", "@googleforcreators/fonts": "*", @@ -54,6 +55,7 @@ "@rollup/plugin-dynamic-import-vars": "^1.4.2", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.2.1", + "@rollup/plugin-typescript": "^8.3.1", "@rollup/plugin-url": "^6.1.0", "@storybook/addon-a11y": "^6.4.20", "@storybook/addon-backgrounds": "^6.4.20", @@ -76,6 +78,7 @@ "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.1.0", + "@typescript-eslint/parser": "^5.19.0", "@web-stories-wp/e2e-tests": "*", "@web-stories-wp/eslint-import-resolver": "*", "@web-stories-wp/jest-amp": "*", @@ -168,6 +171,7 @@ "stylelint-processor-styled-components": "^1.10.0", "stylis-plugin-rtl": "^1.0.0", "terser-webpack-plugin": "^5.1.4", + "typescript": "^4.6.3", "webpack": "^5.71.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -251,7 +255,9 @@ "workflow:assets-version": "commander assets-version", "workflow:version": "commander version", "workflow:build-plugin": "commander build-plugin", - "workflow:bundle-packages": "rollup -c", + "workflow:bundle-packages": "npm-run-all --parallel workflow:bundle-packages:*", + "workflow:bundle-packages:code": "rollup -c", + "workflow:bundle-packages:types": "tsc --build", "workflow:fonts": "npm run update-fonts --workspace packages/fonts", "postworkflow:fonts": "npx prettier --write packages/fonts/src/fonts.json; cp packages/fonts/src/fonts.json includes/data/fonts/fonts.json", "workflow:render-text-sets": "npm run render-text-sets --workspace packages/text-sets", diff --git a/packages/eslint-import-resolver/src/index.cjs b/packages/eslint-import-resolver/src/index.cjs index b8e956dfdcde..bd3afe31427d 100644 --- a/packages/eslint-import-resolver/src/index.cjs +++ b/packages/eslint-import-resolver/src/index.cjs @@ -23,23 +23,26 @@ const nodeResolver = require('eslint-import-resolver-node'); exports.interfaceVersion = 2; exports.resolve = function (source, file, config) { + function nodeResolve(src) { + return nodeResolver.resolve(src, file, { + ...config, + extensions: ['.tsx', '.ts', '.mjs', '.js', '.json', '.node'], + }); + } + if (!config.mapping) { - return nodeResolver.resolve(source, file, config); + return nodeResolve(source); } for (const [regex, dir] of Object.entries(config.mapping)) { // eslint-disable-next-line security/detect-non-literal-regexp const sourceLocation = source.replace(new RegExp(regex), dir); - const result = nodeResolver.resolve( - path.resolve(sourceLocation), - file, - config - ); + const result = nodeResolve(path.resolve(sourceLocation)); if (result.found) { return result; } } - return nodeResolver.resolve(source, file, config); + return nodeResolve(source); }; diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 3661497dc7cc..4bc97ba28709 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -29,12 +29,13 @@ }, "customExports": { ".": { - "default": "./src/index.js" + "default": "./src/index.ts" } }, "main": "dist/index.js", "module": "dist-module/index.js", - "source": "src/index.js", + "types": "dist-types/index.js", + "source": "src/index.ts", "publishConfig": { "access": "public" }, diff --git a/packages/tracking/src/constants.js b/packages/tracking/src/constants.ts similarity index 100% rename from packages/tracking/src/constants.js rename to packages/tracking/src/constants.ts diff --git a/packages/tracking/src/disableTracking.js b/packages/tracking/src/disableTracking.ts similarity index 95% rename from packages/tracking/src/disableTracking.js rename to packages/tracking/src/disableTracking.ts index 1816e90547f3..abf278e7ee29 100644 --- a/packages/tracking/src/disableTracking.js +++ b/packages/tracking/src/disableTracking.ts @@ -19,7 +19,7 @@ */ import { config } from './shared'; -function disableTracking() { +function disableTracking(): void { config.trackingEnabled = false; } diff --git a/packages/tracking/src/enableTracking.js b/packages/tracking/src/enableTracking.ts similarity index 96% rename from packages/tracking/src/enableTracking.js rename to packages/tracking/src/enableTracking.ts index e933b2b62ec3..fc685371763f 100644 --- a/packages/tracking/src/enableTracking.js +++ b/packages/tracking/src/enableTracking.ts @@ -22,7 +22,7 @@ import { config, gtag } from './shared'; const SCRIPT_IDENTIFIER = 'data-web-stories-tracking'; -function loadScriptTag(url) { +function loadScriptTag(url: string) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.setAttribute(SCRIPT_IDENTIFIER, ''); @@ -40,7 +40,7 @@ function loadScriptTag(url) { * @param {boolean} [sendPageView=true] Whether to send a page view event or not upon loading. * @return {Promise} Promise. */ -async function loadTrackingScript(sendPageView = true) { +async function loadTrackingScript(sendPageView: boolean = true) { if (document.querySelector(`script[${SCRIPT_IDENTIFIER}]`)) { return; } @@ -124,7 +124,7 @@ async function loadTrackingScript(sendPageView = true) { }); } -async function enableTracking(sendPageView) { +async function enableTracking(sendPageView: boolean = true) { if (!config.trackingAllowed || config.trackingEnabled) { return; } diff --git a/packages/tracking/src/getTimeTracker.js b/packages/tracking/src/getTimeTracker.ts similarity index 96% rename from packages/tracking/src/getTimeTracker.js rename to packages/tracking/src/getTimeTracker.ts index a39c7101b715..eaaa7f4fae45 100644 --- a/packages/tracking/src/getTimeTracker.js +++ b/packages/tracking/src/getTimeTracker.ts @@ -29,7 +29,7 @@ import { config } from './shared'; * @param {string} eventName The event nae (e.g. 'load_items'). * @return {Function} Callback to stop timer and send tracking event. */ -function getTimeTracker(eventName) { +function getTimeTracker(eventName: string): () => void { const before = window.performance.now(); return () => { const after = window.performance.now(); diff --git a/packages/tracking/src/index.js b/packages/tracking/src/index.ts similarity index 100% rename from packages/tracking/src/index.js rename to packages/tracking/src/index.ts diff --git a/packages/tracking/src/initializeErrorReporting.js b/packages/tracking/src/initializeErrorReporting.ts similarity index 93% rename from packages/tracking/src/initializeErrorReporting.js rename to packages/tracking/src/initializeErrorReporting.ts index 42e6118597ba..5612d4909d6f 100644 --- a/packages/tracking/src/initializeErrorReporting.js +++ b/packages/tracking/src/initializeErrorReporting.ts @@ -25,7 +25,7 @@ const IGNORED_ERROR_MESSAGES = ['ResizeObserver loop limit exceeded']; * * @param {ErrorEvent} event Error event. */ -function handleErrors(event) { +function handleErrors(event: ErrorEvent) { if (event.filename && !event.filename.includes('web-stories')) { return; } @@ -42,7 +42,7 @@ function handleErrors(event) { * * @param {PromiseRejectionEvent} event Promise rejection event. */ -function handleUncaughtPromises(event) { +function handleUncaughtPromises(event: PromiseRejectionEvent) { const errorMessage = event.reason || 'Promise rejection'; trackError('uncaught_promise', errorMessage); } diff --git a/packages/tracking/src/initializeTracking.js b/packages/tracking/src/initializeTracking.ts similarity index 92% rename from packages/tracking/src/initializeTracking.js rename to packages/tracking/src/initializeTracking.ts index a8f0a9af5c31..d8150dfea664 100644 --- a/packages/tracking/src/initializeTracking.js +++ b/packages/tracking/src/initializeTracking.ts @@ -28,7 +28,10 @@ import initializeErrorReporting from './initializeErrorReporting'; * @param {boolean} [sendPageView=true] Whether to send an initial page view event upon loading. * @return {Promise} Promise. */ -async function initializeTracking(appName, sendPageView = true) { +async function initializeTracking( + appName: string, + sendPageView: boolean = true +) { config.appName = appName; await enableTracking(sendPageView); initializeErrorReporting(); diff --git a/packages/tracking/src/isTrackingEnabled.js b/packages/tracking/src/isTrackingEnabled.ts similarity index 100% rename from packages/tracking/src/isTrackingEnabled.js rename to packages/tracking/src/isTrackingEnabled.ts diff --git a/packages/tracking/src/shared.js b/packages/tracking/src/shared.ts similarity index 57% rename from packages/tracking/src/shared.js rename to packages/tracking/src/shared.ts index 92335254c2d4..8f247e0ba754 100644 --- a/packages/tracking/src/shared.js +++ b/packages/tracking/src/shared.ts @@ -19,16 +19,53 @@ */ import { DATA_LAYER } from './constants'; +interface TrackingConfig { + /** + * Whether tracking is allowed in the current context. + * + * @default false + */ + trackingAllowed?: boolean; + /** + * Whether tracking is currently enabled. + * + * @default false + */ + trackingEnabled?: boolean; + /** + * Tracking ID. + */ + trackingId: string; + /** + * GA4 tracking ID. + */ + trackingIdGA4: string; + /** + * Application name. + */ + appName: string; + /** + * Application version. + */ + appVersion: string; + /** + * User properties. + */ + userProperties: Record; +} + /** * Pushes data onto the data layer. * * Must push an instance of Arguments to the target. * Using an ES6 spread operator (i.e. `...args`) will cause tracking events to _silently_ fail. + * + * @param {...any} args Data to send. + * @return {void} */ -export function gtag() { - window[DATA_LAYER] = window[DATA_LAYER] || []; - //eslint-disable-next-line prefer-rest-params -- Must push instead of using spread to prevent tracking failures. - window[DATA_LAYER].push(arguments); +export function gtag(...args: any) { + (window as any)[DATA_LAYER] = (window as any)[DATA_LAYER] || []; + (window as any)[DATA_LAYER].push(args); } const DEFAULT_CONFIG = { @@ -37,6 +74,7 @@ const DEFAULT_CONFIG = { trackingId: '', trackingIdGA4: '', userProperties: {}, + appName: '', }; const { @@ -45,9 +83,9 @@ const { trackingIdGA4, appVersion, userProperties, -} = window.webStoriesTrackingSettings || {}; +} = (window as any).webStoriesTrackingSettings || {}; -export const config = { +export const config: TrackingConfig = { ...DEFAULT_CONFIG, trackingAllowed, trackingId, diff --git a/packages/tracking/src/track.js b/packages/tracking/src/track.ts similarity index 96% rename from packages/tracking/src/track.js rename to packages/tracking/src/track.ts index 4b4acf951118..de3a3eadae10 100644 --- a/packages/tracking/src/track.js +++ b/packages/tracking/src/track.ts @@ -26,7 +26,7 @@ import { gtag } from './shared'; * @param {Object<*>?} [eventData] The event data to send. * @return {Promise} Promise that always resolves. */ -function track(eventName, eventData = {}) { +function track(eventName: string, eventData = {}): Promise { return new Promise((resolve) => { // This timeout ensures a tracking event does not block the user // event if it is not sent (in time). diff --git a/packages/tracking/src/trackClick.js b/packages/tracking/src/trackClick.ts similarity index 88% rename from packages/tracking/src/trackClick.js rename to packages/tracking/src/trackClick.ts index af519ef73d3f..93b888e167d5 100644 --- a/packages/tracking/src/trackClick.js +++ b/packages/tracking/src/trackClick.ts @@ -30,7 +30,7 @@ import track from './track'; * @param {string} eventName The event name (e.g. 'search'). * @return {Promise} Promise that always resolves. */ -async function trackClick(event, eventName) { +async function trackClick(event: MouseEvent, eventName: string) { // currentTarget becomes null after event bubbles up, so we // grab it for reference before any async operations occur. // https://github.com/facebook/react/issues/2857#issuecomment-70006324 @@ -41,11 +41,11 @@ async function trackClick(event, eventName) { } const openLinkInNewTab = - currentTarget?.target === '_blank' || + (currentTarget as HTMLAnchorElement)?.target === '_blank' || event.ctrlKey || event.shiftKey || event.metaKey || - event.which === 2; + event.button === 1; if (openLinkInNewTab) { return track(eventName); @@ -54,7 +54,7 @@ async function trackClick(event, eventName) { event.preventDefault(); return track(eventName).finally(() => { - document.location = currentTarget.href; + document.location = (currentTarget as HTMLAnchorElement).href; }); } diff --git a/packages/tracking/src/trackError.js b/packages/tracking/src/trackError.ts similarity index 93% rename from packages/tracking/src/trackError.js rename to packages/tracking/src/trackError.ts index c75b5a97dc03..1fbbfd8bef65 100644 --- a/packages/tracking/src/trackError.js +++ b/packages/tracking/src/trackError.ts @@ -32,7 +32,11 @@ import track from './track'; * @param {boolean} [fatal=false] Report whether there is a fatal error. * @return {Promise} Promise that always resolves. */ -async function trackError(prefix, description, fatal = false) { +async function trackError( + prefix: string, + description: string, + fatal: boolean = false +): Promise { if (!(await isTrackingEnabled())) { return Promise.resolve(); } diff --git a/packages/tracking/src/trackEvent.js b/packages/tracking/src/trackEvent.ts similarity index 84% rename from packages/tracking/src/trackEvent.js rename to packages/tracking/src/trackEvent.ts index fe9aa09be109..1c1f7e8a25f9 100644 --- a/packages/tracking/src/trackEvent.js +++ b/packages/tracking/src/trackEvent.ts @@ -21,6 +21,18 @@ import { config } from './shared'; import isTrackingEnabled from './isTrackingEnabled'; import track from './track'; +interface EventParameters { + name?: string; + value?: string | number; + send_to?: string | number; + search_type?: string; + duration?: number; + title_length?: number; + unread_count?: number; + event_label?: string; + event_category?: string; +} + /** * Send an Analytics tracking event. * @@ -30,10 +42,13 @@ import track from './track'; * @see https://support.google.com/analytics/answer/9267735 * @see https://support.google.com/analytics/answer/9310895?hl=en * @param {string} eventName The event name (e.g. 'search'). The value that will appear as the event action in Google Analytics Event reports. - * @param {Object<*>} [eventParameters] Event parameters. + * @param {EventParameters} [eventParameters] Event parameters. * @return {Promise} Promise that always resolves. */ -async function trackEvent(eventName, eventParameters = {}) { +async function trackEvent( + eventName: string, + eventParameters: EventParameters = {} +): Promise { if (!(await isTrackingEnabled())) { return Promise.resolve(); } diff --git a/packages/tracking/src/trackScreenView.js b/packages/tracking/src/trackScreenView.ts similarity index 96% rename from packages/tracking/src/trackScreenView.js rename to packages/tracking/src/trackScreenView.ts index bcd23fe803b0..da8cfdf064bb 100644 --- a/packages/tracking/src/trackScreenView.js +++ b/packages/tracking/src/trackScreenView.ts @@ -30,7 +30,7 @@ import isTrackingEnabled from './isTrackingEnabled'; * @param {string} screenName Screen name. Example: 'Explore Templates'. * @return {Promise} Promise that always resolves. */ -async function trackScreenView(screenName) { +async function trackScreenView(screenName: string) { if (!(await isTrackingEnabled())) { return Promise.resolve(); } diff --git a/packages/tracking/src/trackTiming.js b/packages/tracking/src/trackTiming.ts similarity index 92% rename from packages/tracking/src/trackTiming.js rename to packages/tracking/src/trackTiming.ts index 01d1844c3818..390e394eb3b1 100644 --- a/packages/tracking/src/trackTiming.js +++ b/packages/tracking/src/trackTiming.ts @@ -28,7 +28,12 @@ import { config } from './shared'; * @param {string} label Label that allows extra flexibility in reports. * @param {string} eventName Event name, e.g. click or mousedown. */ -function trackTiming(category, time, label = '', eventName = 'click') { +function trackTiming( + category: string, + time: number, + label: string = '', + eventName: string = 'click' +) { // Universal Analytics has a special `timing_complete` event which // does not exist in GA4. trackEvent('timing_complete', { diff --git a/packages/tracking/tsconfig.json b/packages/tracking/tsconfig.json new file mode 100644 index 000000000000..c9d87fc4143c --- /dev/null +++ b/packages/tracking/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "dist-types" + }, + "include": ["src/**/*"] +} diff --git a/rollup.config.js b/rollup.config.js index ffe88e90e0a3..000f698813f7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -22,12 +22,13 @@ import { fileURLToPath } from 'url'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { babel } from '@rollup/plugin-babel'; -import { terser } from 'rollup-plugin-terser'; -import svgr from '@svgr/rollup'; -import filesize from 'rollup-plugin-filesize'; import url from '@rollup/plugin-url'; import json from '@rollup/plugin-json'; import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; +import typescript from '@rollup/plugin-typescript'; +import svgr from '@svgr/rollup'; +import { terser } from 'rollup-plugin-terser'; +import filesize from 'rollup-plugin-filesize'; import license from 'rollup-plugin-license'; import del from 'rollup-plugin-delete'; import copy from 'rollup-plugin-copy'; @@ -42,10 +43,13 @@ const plugins = [ preferBuiltins: true, dedupe: [], }), + typescript(), babel({ + babelrc: false, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs'], babelHelpers: 'inline', exclude: 'node_modules/**', - presets: ['@babel/env', '@babel/preset-react'], + presets: ['@babel/env', '@babel/preset-react', '@babel/preset-typescript'], plugins: [ 'babel-plugin-styled-components', 'babel-plugin-transform-react-remove-prop-types', diff --git a/tests/js/jest.config.js b/tests/js/jest.config.js index f825f31d9eec..8b466cde19ce 100644 --- a/tests/js/jest.config.js +++ b/tests/js/jest.config.js @@ -25,6 +25,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); export default { rootDir: '../../', resolver: '@web-stories-wp/jest-resolver', + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, moduleNameMapper: { '\\.svg': join(__dirname, '/svgrMock.js'), '\\.css': join(__dirname, '/styleMock.js'), diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000000..515bad40da3f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "references": [{ "path": "packages/tracking" }], + "files": [] +} diff --git a/tsconfig.shared.json b/tsconfig.shared.json new file mode 100644 index 000000000000..19d4b41fbb55 --- /dev/null +++ b/tsconfig.shared.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "allowSyntheticDefaultImports": true, + "jsx": "preserve", + "target": "esnext", + "module": "esnext", + "lib": ["dom", "esnext"], + "declaration": true, + "declarationMap": true, + "composite": true, + "emitDeclarationOnly": true, + "isolatedModules": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "importsNotUsedAsValues": "error", + "moduleResolution": "node", + "esModuleInterop": false, + "resolveJsonModule": true, + "typeRoots": ["./typings", "./node_modules/@types"], + "types": [] + }, + "exclude": [ + "packages/*/dist-*/**", + "packages/*/dist/**", + "**/test/**", + "**/karma/**" + ] +} diff --git a/webpack.config.cjs b/webpack.config.cjs index f980dcb6b575..fada76c82361 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -62,6 +62,7 @@ const sharedConfig = { resolve: { // Fixes resolving packages in the monorepo so we use the "src" folder, not "dist". exportsFields: ['customExports', 'exports'], + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.wasm'], }, mode, devtool: !isProduction ? 'source-map' : undefined, @@ -95,7 +96,7 @@ const sharedConfig = { }, }, { - test: /\.m?js$/, + test: /\.(j|t)sx?$/, exclude: /node_modules/, resolve: { // Avoid having to provide full file extension for imports.