mirror of
https://dev.azure.com/tstanciu94/Packages/_git/analytics-switch
synced 2025-08-10 21:14:34 +03:00
Merged PR 106: chore: upgrade package version to 1.1.0 and refactor codebase to TypeScript
chore: upgrade package version to 1.1.0 and refactor codebase to TypeScript - Updated package version in package.json from 1.0.1 to 1.1.0. - Changed main entry point to use built files in dist directory. - Removed Babel configuration and build scripts in favor of tsup. - Deleted copy-files script as it is no longer needed. - Refactored AnalyticsSwitch component from JavaScript to TypeScript. - Added unit tests for AnalyticsSwitch component. - Created new TypeScript files for UmamiAnalytics and MatomoAnalytics components. - Implemented custom hooks for loading Umami and Matomo scripts. - Added TypeScript types for analytics components and props. - Set up TypeScript configuration and build process with tsup. - Configured Vitest for testing with setup files and coverage reporting.
This commit is contained in:
parent
8bebfc142e
commit
14f1b11263
5
.gitignore
vendored
5
.gitignore
vendored
@ -20,4 +20,7 @@ dist
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
example/package-lock.json
|
||||
example/package-lock.json
|
||||
|
||||
.claude
|
||||
CLAUDE.md
|
54
CHANGELOG.md
Normal file
54
CHANGELOG.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.1.0] - 2025-08-10
|
||||
|
||||
### Added
|
||||
|
||||
- 🎯 **TypeScript support** - Complete migration from JavaScript to TypeScript
|
||||
- 📦 **Modern build system** - Replaced Babel with tsup for ultra-fast builds using esbuild
|
||||
- 🧪 **Comprehensive test suite** - Added Vitest with React Testing Library
|
||||
- 📝 **Type definitions** - Full TypeScript type exports for better developer experience
|
||||
- ⚡ **Multiple output formats** - CommonJS, ES Modules, and TypeScript declarations
|
||||
- 🔧 **Modern tooling** - Updated to latest dependencies and build tools
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated from JavaScript to TypeScript with full backward compatibility
|
||||
- Updated build output directory from `build/` to `dist/`
|
||||
- Replaced `prop-types` dependency with TypeScript types
|
||||
- Updated package exports to support both CommonJS and ES Modules
|
||||
- Improved script cleanup logic in analytics hooks
|
||||
- Modernized development scripts and workflows
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed Babel configuration and dependencies in favor of tsup
|
||||
- Removed `prop-types` dependency (replaced with TypeScript types)
|
||||
- Removed custom `copy-files.js` script in favor of modern build system
|
||||
- Removed old build and publish scripts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved error handling for script injection and cleanup
|
||||
- Better memory management for analytics scripts
|
||||
|
||||
## [1.0.1] - 2023-XX-XX
|
||||
|
||||
### Fixed
|
||||
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
## [1.0.0] - 2023-XX-XX
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release of analytics-switch component
|
||||
- Support for Umami analytics integration
|
||||
- Support for Matomo analytics integration
|
||||
- Conditional rendering based on provider configuration
|
||||
- React hooks for script injection and cleanup
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Tudor Stanciu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
201
README.md
201
README.md
@ -1,20 +1,207 @@
|
||||
# analytics-switch
|
||||
# @flare/analytics-switch
|
||||
|
||||
## Install
|
||||
A TypeScript React component library that provides a unified interface for integrating multiple analytics providers (Umami and Matomo) into React applications.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Multi-provider support** - Umami and Matomo analytics in one component
|
||||
- 🔧 **TypeScript** - Full type safety and IntelliSense support
|
||||
- 📦 **Zero dependencies** - Only peer dependencies on React
|
||||
- ⚡ **Performance** - Conditional script loading based on configuration
|
||||
- 🧪 **Tested** - Comprehensive test suite with Vitest
|
||||
- 📱 **Universal** - Works in both browser and server-side rendering environments
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
// with npm
|
||||
# with npm
|
||||
npm i --save @flare/analytics-switch --registry https://lab.code-rove.com/public-node-registry
|
||||
|
||||
// with yarn
|
||||
# with yarn
|
||||
yarn add @flare/analytics-switch --registry https://lab.code-rove.com/public-node-registry
|
||||
|
||||
# with pnpm
|
||||
pnpm add @flare/analytics-switch --registry https://lab.code-rove.com/public-node-registry
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { AnalyticsSwitch } from "@flare/analytics-switch";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<AnalyticsSwitch
|
||||
umami={{
|
||||
enabled: true,
|
||||
source: "https://your-umami-instance.com/script.js",
|
||||
websiteId: "your-website-id"
|
||||
}}
|
||||
matomo={{
|
||||
enabled: true,
|
||||
source: "https://your-matomo-instance.com/",
|
||||
websiteId: "1"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Your app content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**1.0.0**
|
||||
This version includes the initial version of analytics-switch component.
|
||||
### TypeScript Usage
|
||||
|
||||
The component provides full TypeScript support with proper type definitions:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AnalyticsSwitch,
|
||||
type AnalyticsSwitchProps
|
||||
} from "@flare/analytics-switch";
|
||||
|
||||
const analyticsConfig: AnalyticsSwitchProps = {
|
||||
disabled: false, // Optional: disable all analytics
|
||||
umami: {
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
source: "https://analytics.yoursite.com/script.js",
|
||||
websiteId: "abc123"
|
||||
},
|
||||
matomo: {
|
||||
enabled: process.env.NODE_ENV === "production",
|
||||
source: "https://matomo.yoursite.com/",
|
||||
websiteId: "1"
|
||||
}
|
||||
};
|
||||
|
||||
function App() {
|
||||
return <AnalyticsSwitch {...analyticsConfig} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Analytics
|
||||
|
||||
```tsx
|
||||
import { AnalyticsSwitch } from "@flare/analytics-switch";
|
||||
|
||||
function App() {
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
return (
|
||||
<AnalyticsSwitch
|
||||
disabled={!isProduction} // Disable all analytics in non-production
|
||||
umami={{
|
||||
enabled: true,
|
||||
source: "https://umami.example.com/script.js",
|
||||
websiteId: "website-id"
|
||||
}}
|
||||
// Matomo is optional - omit if not needed
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### AnalyticsSwitchProps
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| ---------- | ------------------- | -------- | --------------------------------- |
|
||||
| `disabled` | `boolean` | No | Disable all analytics when `true` |
|
||||
| `umami` | `AnalyticsProvider` | No | Umami analytics configuration |
|
||||
| `matomo` | `AnalyticsProvider` | No | Matomo analytics configuration |
|
||||
|
||||
### AnalyticsProvider
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| ----------- | --------- | -------- | -------------------------------------------- |
|
||||
| `enabled` | `boolean` | Yes | Whether this analytics provider is enabled |
|
||||
| `source` | `string` | Yes | Script source URL for the analytics provider |
|
||||
| `websiteId` | `string` | Yes | Website/site ID for tracking |
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm, yarn, or pnpm
|
||||
|
||||
### Scripts
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build the library
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm run test
|
||||
|
||||
# Run tests with UI
|
||||
npm run test:ui
|
||||
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
|
||||
# Development mode with watch
|
||||
npm run dev
|
||||
|
||||
# Lint and format
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
This library uses [tsup](https://tsup.egoist.dev/) for ultra-fast builds with esbuild:
|
||||
|
||||
- **CommonJS**: `dist/index.js`
|
||||
- **ES Modules**: `dist/index.mjs`
|
||||
- **TypeScript declarations**: `dist/index.d.ts`
|
||||
|
||||
### Testing
|
||||
|
||||
Tests are written with [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/):
|
||||
|
||||
```bash
|
||||
npm test # Run tests
|
||||
npm run test:ui # Run with Vitest UI
|
||||
npm run test:coverage # Run with coverage report
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
```bash
|
||||
# Patch version
|
||||
npm run publish:patch
|
||||
|
||||
# Minor version
|
||||
npm run publish:minor
|
||||
|
||||
# Major version
|
||||
npm run publish:major
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
||||
|
||||
## License
|
||||
|
||||
MIT - see [LICENSE](./LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
7964
package-lock.json
generated
7964
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
77
package.json
77
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@flare/analytics-switch",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"description": "Analytics switch that includes integrations with Umami and Matomo",
|
||||
"author": {
|
||||
"name": "Tudor Stanciu",
|
||||
@ -19,48 +19,55 @@
|
||||
"umami",
|
||||
"matomo"
|
||||
],
|
||||
"main": "./src/index.js",
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-react"
|
||||
]
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "rimraf build",
|
||||
"build": "npm run build:cjs && npm run build:copy-files",
|
||||
"build:cjs": "babel src -d build --copy-files",
|
||||
"build:copy-files": "node ./scripts/copy-files.js",
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "prettier --check .",
|
||||
"lint:fix": "prettier --write .",
|
||||
"precommit": "lint-staged",
|
||||
"prepare": "husky install",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepush": "npm run build",
|
||||
"push": "cd build && npm publish --registry https://lab.code-rove.com/public-node-registry",
|
||||
"push:major": "npm run version:major && npm run push",
|
||||
"push:minor": "npm run version:minor && npm run push",
|
||||
"push:patch": "npm run version:patch && npm run push",
|
||||
"version:major": "npm version major --no-git-tag-version",
|
||||
"version:minor": "npm version minor --no-git-tag-version",
|
||||
"version:patch": "npm version patch --no-git-tag-version"
|
||||
"push": "npm publish --registry https://lab.code-rove.com/public-node-registry",
|
||||
"prepublishOnly": "npm test && npm run build",
|
||||
"publish:patch": "npm version patch && npm run push",
|
||||
"publish:minor": "npm version minor && npm run push",
|
||||
"publish:major": "npm version major && npm run push"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.7",
|
||||
"@babel/core": "^7.16.7",
|
||||
"@babel/node": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.7",
|
||||
"@babel/preset-react": "^7.8.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.2.2",
|
||||
"prettier": "^2.5.1"
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
"@vitest/ui": "^2.1.8",
|
||||
"husky": "^9.1.6",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.3.3",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.6.3",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "none",
|
||||
|
@ -1,87 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const path = require("path");
|
||||
const fse = require("fs-extra");
|
||||
const glob = require("glob");
|
||||
|
||||
const packagePath = process.cwd();
|
||||
const buildPath = path.join(packagePath, "./build");
|
||||
const srcPath = path.join(packagePath, "./src");
|
||||
|
||||
/**
|
||||
* Puts a package.json into every immediate child directory of rootDir.
|
||||
* That package.json contains information about esm for bundlers so that imports
|
||||
* like import Typography from '@material-ui/core/Typography' are tree-shakeable.
|
||||
*
|
||||
* It also tests that an this import can be used in typescript by checking
|
||||
* if an index.d.ts is present at that path.
|
||||
*
|
||||
* @param {string} rootDir
|
||||
*/
|
||||
// async function createModulePackages({ from, to }) {
|
||||
// const directoryPackages = glob.sync('*/index.js', { cwd: from }).map(path.dirname);
|
||||
|
||||
// await Promise.all(
|
||||
// directoryPackages.map(async directoryPackage => {
|
||||
// const packageJson = {
|
||||
// sideEffects: false,
|
||||
// module: path.join('../esm', directoryPackage, 'index.js'),
|
||||
// typings: './index.d.ts',
|
||||
// };
|
||||
// const packageJsonPath = path.join(to, directoryPackage, 'package.json');
|
||||
|
||||
// /*const [typingsExist] =*/ await Promise.all([
|
||||
// //fse.exists(path.join(to, directoryPackage, 'index.d.ts')),
|
||||
// fse.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)),
|
||||
// ]);
|
||||
|
||||
// // if (!typingsExist) {
|
||||
// // throw new Error(`index.d.ts for ${directoryPackage} is missing`);
|
||||
// // }
|
||||
|
||||
// return packageJsonPath;
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
async function createPackageFile() {
|
||||
const packageData = await fse.readFile(
|
||||
path.resolve(packagePath, "./package.json"),
|
||||
"utf8"
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { nyc, scripts, devDependencies, workspaces, ...packageDataOther } =
|
||||
JSON.parse(packageData);
|
||||
const newPackageData = {
|
||||
...packageDataOther,
|
||||
private: false,
|
||||
main: "./index.js"
|
||||
};
|
||||
const targetPath = path.resolve(buildPath, "./package.json");
|
||||
|
||||
await fse.writeFile(
|
||||
targetPath,
|
||||
JSON.stringify(newPackageData, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
console.log(`Created package.json in ${targetPath}`);
|
||||
|
||||
fse.copy("./README.md", `${buildPath}/README.md`, err => {
|
||||
if (err) throw err;
|
||||
console.log("README file was copied to destination");
|
||||
});
|
||||
|
||||
return newPackageData;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await createPackageFile();
|
||||
|
||||
//await createModulePackages({ from: srcPath, to: buildPath });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
@ -1,35 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import UmamiAnalytics from "./umami/UmamiAnalytics";
|
||||
import MatomoAnalytics from "./matomo/MatomoAnalytics";
|
||||
|
||||
const AnalyticsSwitch = ({ disabled, umami, matomo }) => {
|
||||
if (disabled) return "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{umami && umami.enabled && <UmamiAnalytics {...umami} />}
|
||||
{matomo && matomo.enabled && <MatomoAnalytics {...matomo} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AnalyticsSwitch.defaultProps = {
|
||||
disabled: false
|
||||
};
|
||||
|
||||
AnalyticsSwitch.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
umami: PropTypes.shape({
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
websiteId: PropTypes.string.isRequired
|
||||
}),
|
||||
matomo: PropTypes.shape({
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
websiteId: PropTypes.string.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default AnalyticsSwitch;
|
21
src/AnalyticsSwitch.tsx
Normal file
21
src/AnalyticsSwitch.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import UmamiAnalytics from "./umami/UmamiAnalytics";
|
||||
import MatomoAnalytics from "./matomo/MatomoAnalytics";
|
||||
import type { AnalyticsSwitchProps } from "./types";
|
||||
|
||||
const AnalyticsSwitch: React.FC<AnalyticsSwitchProps> = ({
|
||||
disabled = false,
|
||||
umami,
|
||||
matomo
|
||||
}) => {
|
||||
if (disabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{umami?.enabled && <UmamiAnalytics {...umami} />}
|
||||
{matomo?.enabled && <MatomoAnalytics {...matomo} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsSwitch;
|
117
src/__tests__/AnalyticsSwitch.test.tsx
Normal file
117
src/__tests__/AnalyticsSwitch.test.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import AnalyticsSwitch from "../AnalyticsSwitch";
|
||||
import type { AnalyticsSwitchProps } from "../types";
|
||||
|
||||
// Mock the analytics components
|
||||
vi.mock("../umami/UmamiAnalytics", () => ({
|
||||
default: ({ source, websiteId }: { source: string; websiteId: string }) => (
|
||||
<div
|
||||
data-testid="umami-analytics"
|
||||
data-source={source}
|
||||
data-website-id={websiteId}
|
||||
/>
|
||||
)
|
||||
}));
|
||||
|
||||
vi.mock("../matomo/MatomoAnalytics", () => ({
|
||||
default: ({ source, websiteId }: { source: string; websiteId: string }) => (
|
||||
<div
|
||||
data-testid="matomo-analytics"
|
||||
data-source={source}
|
||||
data-website-id={websiteId}
|
||||
/>
|
||||
)
|
||||
}));
|
||||
|
||||
describe("AnalyticsSwitch", () => {
|
||||
it("renders nothing when disabled", () => {
|
||||
const props: AnalyticsSwitchProps = {
|
||||
disabled: true,
|
||||
umami: {
|
||||
enabled: true,
|
||||
source: "https://umami.example.com",
|
||||
websiteId: "test-id"
|
||||
}
|
||||
};
|
||||
|
||||
const { container } = render(<AnalyticsSwitch {...props} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders Umami analytics when enabled", () => {
|
||||
const props: AnalyticsSwitchProps = {
|
||||
umami: {
|
||||
enabled: true,
|
||||
source: "https://umami.example.com/script.js",
|
||||
websiteId: "umami-test-id"
|
||||
}
|
||||
};
|
||||
|
||||
render(<AnalyticsSwitch {...props} />);
|
||||
|
||||
const umamiElement = screen.getByTestId("umami-analytics");
|
||||
expect(umamiElement).toHaveAttribute(
|
||||
"data-source",
|
||||
"https://umami.example.com/script.js"
|
||||
);
|
||||
expect(umamiElement).toHaveAttribute("data-website-id", "umami-test-id");
|
||||
});
|
||||
|
||||
it("renders Matomo analytics when enabled", () => {
|
||||
const props: AnalyticsSwitchProps = {
|
||||
matomo: {
|
||||
enabled: true,
|
||||
source: "https://matomo.example.com/",
|
||||
websiteId: "matomo-test-id"
|
||||
}
|
||||
};
|
||||
|
||||
render(<AnalyticsSwitch {...props} />);
|
||||
|
||||
const matomoElement = screen.getByTestId("matomo-analytics");
|
||||
expect(matomoElement).toHaveAttribute(
|
||||
"data-source",
|
||||
"https://matomo.example.com/"
|
||||
);
|
||||
expect(matomoElement).toHaveAttribute("data-website-id", "matomo-test-id");
|
||||
});
|
||||
|
||||
it("renders both analytics when both are enabled", () => {
|
||||
const props: AnalyticsSwitchProps = {
|
||||
umami: {
|
||||
enabled: true,
|
||||
source: "https://umami.example.com/script.js",
|
||||
websiteId: "umami-test-id"
|
||||
},
|
||||
matomo: {
|
||||
enabled: true,
|
||||
source: "https://matomo.example.com/",
|
||||
websiteId: "matomo-test-id"
|
||||
}
|
||||
};
|
||||
|
||||
render(<AnalyticsSwitch {...props} />);
|
||||
|
||||
expect(screen.getByTestId("umami-analytics")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("matomo-analytics")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render analytics when they are disabled", () => {
|
||||
const props: AnalyticsSwitchProps = {
|
||||
umami: {
|
||||
enabled: false,
|
||||
source: "https://umami.example.com/script.js",
|
||||
websiteId: "umami-test-id"
|
||||
},
|
||||
matomo: {
|
||||
enabled: false,
|
||||
source: "https://matomo.example.com/",
|
||||
websiteId: "matomo-test-id"
|
||||
}
|
||||
};
|
||||
|
||||
const { container } = render(<AnalyticsSwitch {...props} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
1
src/__tests__/setup.ts
Normal file
1
src/__tests__/setup.ts
Normal file
@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
84
src/__tests__/useMatomoScript.test.ts
Normal file
84
src/__tests__/useMatomoScript.test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import useMatomoScript from "../matomo/useMatomoScript";
|
||||
|
||||
describe("useMatomoScript", () => {
|
||||
beforeEach(() => {
|
||||
// Clear any existing scripts
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
it("creates and appends Matomo script to document head", () => {
|
||||
const source = "https://matomo.example.com/";
|
||||
const websiteId = "1";
|
||||
|
||||
renderHook(() => useMatomoScript(source, websiteId));
|
||||
|
||||
const scripts = document.head.querySelectorAll("script");
|
||||
expect(scripts.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Find the inline script that was injected by our hook
|
||||
const inlineScript = Array.from(scripts).find(script =>
|
||||
script.text.includes("_paq")
|
||||
);
|
||||
expect(inlineScript).toBeDefined();
|
||||
expect(inlineScript!.text).toContain(
|
||||
"var _paq = (window._paq = window._paq || []);"
|
||||
);
|
||||
expect(inlineScript!.text).toContain(`var u = "${source}";`);
|
||||
expect(inlineScript!.text).toContain(
|
||||
`_paq.push(["setSiteId", "${websiteId}"]);`
|
||||
);
|
||||
});
|
||||
|
||||
it("removes script when component unmounts", () => {
|
||||
const source = "https://matomo.example.com/";
|
||||
const websiteId = "1";
|
||||
|
||||
const { unmount } = renderHook(() => useMatomoScript(source, websiteId));
|
||||
|
||||
const initialScriptCount = document.head.querySelectorAll("script").length;
|
||||
expect(initialScriptCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
unmount();
|
||||
|
||||
const finalScriptCount = document.head.querySelectorAll("script").length;
|
||||
expect(finalScriptCount).toBeLessThan(initialScriptCount);
|
||||
});
|
||||
|
||||
it("updates script when source or websiteId changes", () => {
|
||||
const initialSource = "https://matomo.example.com/";
|
||||
const initialWebsiteId = "1";
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ source, websiteId }) => useMatomoScript(source, websiteId),
|
||||
{ initialProps: { source: initialSource, websiteId: initialWebsiteId } }
|
||||
);
|
||||
|
||||
expect(
|
||||
document.head.querySelectorAll("script").length
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const newSource = "https://new-matomo.example.com/";
|
||||
const newWebsiteId = "2";
|
||||
|
||||
rerender({ source: newSource, websiteId: newWebsiteId });
|
||||
|
||||
const scripts = document.head.querySelectorAll("script");
|
||||
expect(scripts.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Find the current inline script
|
||||
const currentScript = Array.from(scripts).find(script =>
|
||||
script.text.includes("_paq")
|
||||
);
|
||||
expect(currentScript).toBeDefined();
|
||||
expect(currentScript!.text).toContain(`var u = "${newSource}";`);
|
||||
expect(currentScript!.text).toContain(
|
||||
`_paq.push(["setSiteId", "${newWebsiteId}"]);`
|
||||
);
|
||||
});
|
||||
});
|
67
src/__tests__/useUmamiScript.test.ts
Normal file
67
src/__tests__/useUmamiScript.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import useUmamiScript from "../umami/useUmamiScript";
|
||||
|
||||
describe("useUmamiScript", () => {
|
||||
beforeEach(() => {
|
||||
// Clear any existing scripts
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
it("creates and appends Umami script to document head", () => {
|
||||
const source = "https://umami.example.com/script.js";
|
||||
const websiteId = "test-website-id";
|
||||
|
||||
renderHook(() => useUmamiScript(source, websiteId));
|
||||
|
||||
const scripts = document.head.querySelectorAll("script");
|
||||
expect(scripts).toHaveLength(1);
|
||||
|
||||
const script = scripts[0];
|
||||
expect(script.src).toBe(source);
|
||||
expect(script.getAttribute("data-website-id")).toBe(websiteId);
|
||||
expect(script.async).toBe(true);
|
||||
expect(script.defer).toBe(true);
|
||||
});
|
||||
|
||||
it("removes script when component unmounts", () => {
|
||||
const source = "https://umami.example.com/script.js";
|
||||
const websiteId = "test-website-id";
|
||||
|
||||
const { unmount } = renderHook(() => useUmamiScript(source, websiteId));
|
||||
|
||||
expect(document.head.querySelectorAll("script")).toHaveLength(1);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.head.querySelectorAll("script")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("updates script when source or websiteId changes", () => {
|
||||
const initialSource = "https://umami.example.com/script.js";
|
||||
const initialWebsiteId = "test-website-id-1";
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ source, websiteId }) => useUmamiScript(source, websiteId),
|
||||
{ initialProps: { source: initialSource, websiteId: initialWebsiteId } }
|
||||
);
|
||||
|
||||
expect(document.head.querySelectorAll("script")).toHaveLength(1);
|
||||
|
||||
const newSource = "https://new-umami.example.com/script.js";
|
||||
const newWebsiteId = "test-website-id-2";
|
||||
|
||||
rerender({ source: newSource, websiteId: newWebsiteId });
|
||||
|
||||
const scripts = document.head.querySelectorAll("script");
|
||||
expect(scripts).toHaveLength(1);
|
||||
|
||||
const script = scripts[0];
|
||||
expect(script.src).toBe(newSource);
|
||||
expect(script.getAttribute("data-website-id")).toBe(newWebsiteId);
|
||||
});
|
||||
});
|
@ -2,3 +2,8 @@ import AnalyticsSwitch from "./AnalyticsSwitch";
|
||||
|
||||
export { AnalyticsSwitch };
|
||||
export default AnalyticsSwitch;
|
||||
export type {
|
||||
AnalyticsSwitchProps,
|
||||
AnalyticsProvider,
|
||||
AnalyticsComponentProps
|
||||
} from "./types";
|
@ -1,14 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import useMatomoScript from "./useMatomoScript";
|
||||
|
||||
const MatomoAnalytics = ({ source, websiteId }) => {
|
||||
useMatomoScript(source, websiteId);
|
||||
return "";
|
||||
};
|
||||
|
||||
MatomoAnalytics.propTypes = {
|
||||
source: PropTypes.string.isRequired,
|
||||
websiteId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default MatomoAnalytics;
|
13
src/matomo/MatomoAnalytics.tsx
Normal file
13
src/matomo/MatomoAnalytics.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import useMatomoScript from "./useMatomoScript";
|
||||
import type { AnalyticsComponentProps } from "../types";
|
||||
|
||||
const MatomoAnalytics: React.FC<AnalyticsComponentProps> = ({
|
||||
source,
|
||||
websiteId
|
||||
}) => {
|
||||
useMatomoScript(source, websiteId);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MatomoAnalytics;
|
@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useMatomoScript = (source, websiteId) => {
|
||||
const useMatomoScript = (source: string, websiteId: string): void => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
script.text = `var _paq = (window._paq = window._paq || []);
|
||||
@ -22,7 +22,9 @@ const useMatomoScript = (source, websiteId) => {
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
if (document.head.contains(script)) {
|
||||
document.head.removeChild(script);
|
||||
}
|
||||
};
|
||||
}, [source, websiteId]);
|
||||
};
|
16
src/types.ts
Normal file
16
src/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface AnalyticsProvider {
|
||||
enabled: boolean;
|
||||
source: string;
|
||||
websiteId: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsSwitchProps {
|
||||
disabled?: boolean;
|
||||
umami?: AnalyticsProvider;
|
||||
matomo?: AnalyticsProvider;
|
||||
}
|
||||
|
||||
export interface AnalyticsComponentProps {
|
||||
source: string;
|
||||
websiteId: string;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import useUmamiScript from "./useUmamiScript";
|
||||
|
||||
const UmamiAnalytics = ({ source, websiteId }) => {
|
||||
useUmamiScript(source, websiteId);
|
||||
return "";
|
||||
};
|
||||
|
||||
UmamiAnalytics.propTypes = {
|
||||
source: PropTypes.string.isRequired,
|
||||
websiteId: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default UmamiAnalytics;
|
13
src/umami/UmamiAnalytics.tsx
Normal file
13
src/umami/UmamiAnalytics.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import useUmamiScript from "./useUmamiScript";
|
||||
import type { AnalyticsComponentProps } from "../types";
|
||||
|
||||
const UmamiAnalytics: React.FC<AnalyticsComponentProps> = ({
|
||||
source,
|
||||
websiteId
|
||||
}) => {
|
||||
useUmamiScript(source, websiteId);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UmamiAnalytics;
|
@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useUmamiScript = (src, websiteId) => {
|
||||
const useUmamiScript = (src: string, websiteId: string): void => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
script.async = true;
|
||||
@ -11,7 +11,9 @@ const useUmamiScript = (src, websiteId) => {
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
if (document.head.contains(script)) {
|
||||
document.head.removeChild(script);
|
||||
}
|
||||
};
|
||||
}, [src, websiteId]);
|
||||
};
|
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Declaration */
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.*", "**/*.spec.*"]
|
||||
}
|
13
tsup.config.ts
Normal file
13
tsup.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ["react", "react-dom"],
|
||||
esbuildOptions: options => {
|
||||
options.jsx = "automatic";
|
||||
}
|
||||
});
|
16
vitest.config.ts
Normal file
16
vitest.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./src/__tests__/setup.ts"],
|
||||
coverage: {
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["node_modules/", "src/__tests__/"]
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user