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:
Tudor Stanciu 2025-08-10 18:02:52 +00:00
parent 8bebfc142e
commit 14f1b11263
24 changed files with 5837 additions and 3044 deletions

5
.gitignore vendored
View File

@ -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
View 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
View 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
View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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();

View File

@ -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
View 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;

View 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
View File

@ -0,0 +1 @@
import "@testing-library/jest-dom";

View 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}"]);`
);
});
});

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

View File

@ -2,3 +2,8 @@ import AnalyticsSwitch from "./AnalyticsSwitch";
export { AnalyticsSwitch };
export default AnalyticsSwitch;
export type {
AnalyticsSwitchProps,
AnalyticsProvider,
AnalyticsComponentProps
} from "./types";

View File

@ -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;

View 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;

View File

@ -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
View 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;
}

View File

@ -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;

View 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;

View File

@ -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
View 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
View 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
View 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__/"]
}
}
});