@mayson-org/inject-script 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @mayson/inject-script
2
+
3
+ A lightweight CLI tool and Node.js package designed for **Next.js App Router** applications. It dynamically locates the project's root layout file (regardless of whether it's stored in `app/layout.tsx`, `src/app/layout.tsx`, or inside route groups like `app/(main)/layout.js`) and injects a floating `<iframe>` pointing to `https://mayson.dev/`.
4
+
5
+ ---
6
+
7
+ ## Quick Usage via `npx`
8
+
9
+ Run directly in any Next.js project root:
10
+
11
+ ```bash
12
+ npx @mayson/inject-script
13
+ ```
14
+
15
+ Or pass optional flags:
16
+
17
+ ```bash
18
+ # Specify custom iframe URL
19
+ npx @mayson/inject-script --url=https://mayson.dev/my-app
20
+
21
+ # Dry-run mode (preview changes without modifying files)
22
+ npx @mayson/inject-script --dry-run
23
+
24
+ # Specify target project directory
25
+ npx @mayson/inject-script --dir=./my-nextjs-app
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Adding to Next.js `package.json` Build Scripts
31
+
32
+ You can automate this script during deployment builds (e.g. on Vercel, Netlify, Docker, or GitHub Actions) by adding it to `package.json`:
33
+
34
+ ```json
35
+ {
36
+ "scripts": {
37
+ "dev": "next dev",
38
+ "prebuild": "npx @mayson/inject-script",
39
+ "build": "next build"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Programmatic API Usage
47
+
48
+ You can also import and use `inject-script` in custom build scripts:
49
+
50
+ ```javascript
51
+ import { runAutoInject, findRootLayout, injectMaysonIframe } from 'inject-script';
52
+
53
+ // 1. Auto-detect and inject in one call
54
+ const result = runAutoInject(process.cwd(), 'https://mayson.dev/');
55
+
56
+ if (result.success) {
57
+ console.log(`Injected to: ${result.filePath}`);
58
+ }
59
+
60
+ // 2. Or manually find layout and inspect
61
+ const rootLayoutPath = findRootLayout('./my-project');
62
+ console.log('Root Layout found at:', rootLayoutPath);
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Features
68
+
69
+ - 🔍 **Dynamic Root Layout Resolution**: Scans `app/`, `src/app/`, and nested route groups for layout files and scores candidates based on HTML/Body root tags.
70
+ - ⚡ **Idempotent**: Safe to run multiple times without creating duplicate iframe injections.
71
+ - 🛡️ **Zero Runtime Overhead**: Inserts a self-contained React `<iframe />` directly into the JSX tree.
package/bin/cli.mjs ADDED
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+
6
+ function findLayoutFiles(dir, fileList = []) {
7
+ if (!fs.existsSync(dir)) return fileList;
8
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
14
+ findLayoutFiles(fullPath, fileList);
15
+ } else if (entry.isFile()) {
16
+ const ext = path.extname(entry.name);
17
+ const base = path.basename(entry.name, ext);
18
+ if (base === 'layout' && ['.tsx', '.jsx', '.ts', '.js'].includes(ext)) {
19
+ fileList.push(fullPath);
20
+ }
21
+ }
22
+ }
23
+ return fileList;
24
+ }
25
+
26
+ function scoreLayout(filePath, projectRoot) {
27
+ const content = fs.readFileSync(filePath, 'utf-8');
28
+ const relativePath = path.relative(projectRoot, filePath);
29
+ const depth = relativePath.split(path.sep).length;
30
+ let score = 100 - depth;
31
+
32
+ if (/<html/i.test(content)) score += 50;
33
+ if (/<body/i.test(content)) score += 50;
34
+ if (/^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) || /^app[/\\]layout\.[jt]sx?$/.test(relativePath)) {
35
+ score += 40;
36
+ }
37
+ return score;
38
+ }
39
+
40
+ function findRootLayout(projectRoot) {
41
+ const possibleDirs = [path.join(projectRoot, 'src', 'app'), path.join(projectRoot, 'app')];
42
+ const candidates = [];
43
+
44
+ for (const dir of possibleDirs) {
45
+ if (fs.existsSync(dir)) findLayoutFiles(dir, candidates);
46
+ }
47
+
48
+ if (candidates.length === 0) return null;
49
+ candidates.sort((a, b) => scoreLayout(b, projectRoot) - scoreLayout(a, projectRoot));
50
+ return candidates[0];
51
+ }
52
+
53
+ function parseArgs() {
54
+ const args = process.argv.slice(2);
55
+ const options = {
56
+ projectRoot: process.cwd(),
57
+ url: 'https://mayson.dev/',
58
+ dryRun: false,
59
+ help: false,
60
+ };
61
+
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+ if (arg === '--help' || arg === '-h') {
65
+ options.help = true;
66
+ } else if (arg === '--dry-run') {
67
+ options.dryRun = true;
68
+ } else if (arg.startsWith('--url=')) {
69
+ options.url = arg.split('=')[1];
70
+ } else if (arg === '--url' && args[i + 1]) {
71
+ options.url = args[++i];
72
+ } else if (arg.startsWith('--dir=')) {
73
+ options.projectRoot = path.resolve(arg.split('=')[1]);
74
+ } else if (arg === '--dir' && args[i + 1]) {
75
+ options.projectRoot = path.resolve(args[++i]);
76
+ }
77
+ }
78
+ return options;
79
+ }
80
+
81
+ const options = parseArgs();
82
+
83
+ if (options.help) {
84
+ console.log(`
85
+ Inject Script CLI - Next.js App Router Root Layout Iframe Injector
86
+
87
+ Usage:
88
+ npx inject-script [options]
89
+
90
+ Options:
91
+ --dir <path> Target project root directory (default: current working directory)
92
+ --url <url> Iframe URL to inject (default: https://mayson.dev/)
93
+ --dry-run Simulate layout detection and code injection without modifying files
94
+ --help, -h Show this help message
95
+ `);
96
+ process.exit(0);
97
+ }
98
+
99
+ console.log(`🔍 Searching for Next.js root layout in: ${options.projectRoot}`);
100
+
101
+ const layoutFile = findRootLayout(options.projectRoot);
102
+
103
+ if (!layoutFile) {
104
+ console.error(`❌ Error: Could not find any App Router layout file in ${options.projectRoot}`);
105
+ process.exit(1);
106
+ }
107
+
108
+ console.log(`📍 Found Root Layout: ${path.relative(options.projectRoot, layoutFile)}`);
109
+
110
+ const content = fs.readFileSync(layoutFile, 'utf-8');
111
+ const INJECTION_MARKER = '/* @mayson-iframe-injected */';
112
+
113
+ if (content.includes(INJECTION_MARKER) || content.includes(options.url)) {
114
+ console.log(`✅ Mayson iframe is already injected in ${path.basename(layoutFile)}.`);
115
+ process.exit(0);
116
+ }
117
+
118
+ const snippet = `
119
+ {${INJECTION_MARKER}}
120
+ <iframe
121
+ src="${options.url}"
122
+ style={{
123
+ position: 'fixed',
124
+ bottom: '20px',
125
+ right: '20px',
126
+ width: '400px',
127
+ height: '600px',
128
+ border: 'none',
129
+ borderRadius: '12px',
130
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.25)',
131
+ zIndex: 999999,
132
+ }}
133
+ title="Mayson Dev Overlay"
134
+ />`;
135
+
136
+ let updatedContent = null;
137
+
138
+ if (/<\/body>/i.test(content)) {
139
+ updatedContent = content.replace(/<\/body>/i, `${snippet}\n </body>`);
140
+ } else if (/<\/html>/i.test(content)) {
141
+ updatedContent = content.replace(/<\/html>/i, `${snippet}\n </html>`);
142
+ } else {
143
+ const lastIndex = content.lastIndexOf('</');
144
+ if (lastIndex !== -1) {
145
+ updatedContent = content.slice(0, lastIndex) + snippet + '\n ' + content.slice(lastIndex);
146
+ }
147
+ }
148
+
149
+ if (!updatedContent) {
150
+ console.error(`❌ Error: Could not find suitable insertion point in ${layoutFile}`);
151
+ process.exit(1);
152
+ }
153
+
154
+ if (options.dryRun) {
155
+ console.log(`🧪 [Dry Run] Would inject iframe to ${layoutFile}`);
156
+ } else {
157
+ fs.writeFileSync(layoutFile, updatedContent, 'utf-8');
158
+ console.log(`🎉 Successfully injected Mayson iframe (${options.url}) into ${path.relative(options.projectRoot, layoutFile)}!`);
159
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Finds the most likely Next.js App Router root layout file in a project.
3
+ */
4
+ export declare function findRootLayout(projectRoot?: string): string | null;
5
+ //# sourceMappingURL=find-root-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-root-layout.d.ts","sourceRoot":"","sources":["../src/find-root-layout.ts"],"names":[],"mappings":"AAyDA;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA2BjF"}
@@ -0,0 +1,75 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
4
+ /**
5
+ * Recursively search a directory for layout files.
6
+ */
7
+ function findLayoutFiles(dir, fileList = []) {
8
+ if (!fs.existsSync(dir))
9
+ return fileList;
10
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
11
+ for (const entry of entries) {
12
+ const fullPath = path.join(dir, entry.name);
13
+ if (entry.isDirectory()) {
14
+ // Don't traverse node_modules or hidden folders (.next, .git)
15
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
16
+ continue;
17
+ }
18
+ findLayoutFiles(fullPath, fileList);
19
+ }
20
+ else if (entry.isFile()) {
21
+ const ext = path.extname(entry.name);
22
+ const base = path.basename(entry.name, ext);
23
+ if (base === 'layout' && LAYOUT_EXTENSIONS.includes(ext)) {
24
+ fileList.push(fullPath);
25
+ }
26
+ }
27
+ }
28
+ return fileList;
29
+ }
30
+ /**
31
+ * Scores a layout file candidate to determine if it is the root layout.
32
+ * Higher score = higher likelihood of being the root layout.
33
+ */
34
+ function scoreLayoutCandidate(filePath, projectRoot) {
35
+ const content = fs.readFileSync(filePath, 'utf-8');
36
+ const relativePath = path.relative(projectRoot, filePath);
37
+ const depth = relativePath.split(path.sep).length;
38
+ let score = 100 - depth; // Shorter path depth is preferred
39
+ // Check for HTML/Body root tags
40
+ if (/<html/i.test(content))
41
+ score += 50;
42
+ if (/<body/i.test(content))
43
+ score += 50;
44
+ // Check if located directly under app/ or src/app/
45
+ const isDirectAppChild = /^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
46
+ /^app[/\\]layout\.[jt]sx?$/.test(relativePath);
47
+ if (isDirectAppChild)
48
+ score += 40;
49
+ return score;
50
+ }
51
+ /**
52
+ * Finds the most likely Next.js App Router root layout file in a project.
53
+ */
54
+ export function findRootLayout(projectRoot = process.cwd()) {
55
+ const possibleAppDirs = [
56
+ path.join(projectRoot, 'src', 'app'),
57
+ path.join(projectRoot, 'app'),
58
+ ];
59
+ const candidateFiles = [];
60
+ for (const appDir of possibleAppDirs) {
61
+ if (fs.existsSync(appDir)) {
62
+ findLayoutFiles(appDir, candidateFiles);
63
+ }
64
+ }
65
+ if (candidateFiles.length === 0) {
66
+ return null;
67
+ }
68
+ // Score each candidate to pick the true root layout
69
+ const scored = candidateFiles.map((filePath) => ({
70
+ filePath,
71
+ score: scoreLayoutCandidate(filePath, projectRoot),
72
+ }));
73
+ scored.sort((a, b) => b.score - a.score);
74
+ return scored[0]?.filePath || null;
75
+ }
@@ -0,0 +1,9 @@
1
+ import { findRootLayout } from './find-root-layout.js';
2
+ import { injectMaysonIframe, InjectorOptions, InjectorResult } from './injector.js';
3
+ export { findRootLayout, injectMaysonIframe };
4
+ export type { InjectorOptions, InjectorResult };
5
+ /**
6
+ * Convenience function to automatically locate and inject the Mayson iframe into a Next.js project.
7
+ */
8
+ export declare function runAutoInject(projectRoot?: string, iframeUrl?: string, dryRun?: boolean): InjectorResult;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpF,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAEhD;;GAEG;AACH,wBAAgB,aAAa,CAAC,WAAW,GAAE,MAAsB,EAAE,SAAS,GAAE,MAA8B,EAAE,MAAM,GAAE,OAAe,GAAG,cAAc,CAiBrJ"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import { findRootLayout } from './find-root-layout.js';
2
+ import { injectMaysonIframe } from './injector.js';
3
+ export { findRootLayout, injectMaysonIframe };
4
+ /**
5
+ * Convenience function to automatically locate and inject the Mayson iframe into a Next.js project.
6
+ */
7
+ export function runAutoInject(projectRoot = process.cwd(), iframeUrl = 'https://mayson.dev/', dryRun = false) {
8
+ const layoutPath = findRootLayout(projectRoot);
9
+ if (!layoutPath) {
10
+ return {
11
+ success: false,
12
+ filePath: '',
13
+ alreadyInjected: false,
14
+ error: `Could not locate Next.js App Router root layout file in ${projectRoot}`,
15
+ };
16
+ }
17
+ return injectMaysonIframe({
18
+ filePath: layoutPath,
19
+ iframeUrl,
20
+ dryRun,
21
+ });
22
+ }
@@ -0,0 +1,14 @@
1
+ export interface InjectorOptions {
2
+ filePath: string;
3
+ iframeUrl?: string;
4
+ dryRun?: boolean;
5
+ }
6
+ export interface InjectorResult {
7
+ success: boolean;
8
+ filePath: string;
9
+ alreadyInjected: boolean;
10
+ modifiedContent?: string;
11
+ error?: string;
12
+ }
13
+ export declare function injectMaysonIframe(options: InjectorOptions): InjectorResult;
14
+ //# sourceMappingURL=injector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injector.d.ts","sourceRoot":"","sources":["../src/injector.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CAkF3E"}
@@ -0,0 +1,76 @@
1
+ import fs from 'fs';
2
+ const INJECTION_MARKER = '/* @mayson-iframe-injected */';
3
+ export function injectMaysonIframe(options) {
4
+ const { filePath, iframeUrl = 'https://mayson.dev/', dryRun = false } = options;
5
+ if (!fs.existsSync(filePath)) {
6
+ return {
7
+ success: false,
8
+ filePath,
9
+ alreadyInjected: false,
10
+ error: `File not found: ${filePath}`,
11
+ };
12
+ }
13
+ const content = fs.readFileSync(filePath, 'utf-8');
14
+ // Idempotency check: Don't inject twice
15
+ if (content.includes(INJECTION_MARKER) || content.includes(iframeUrl)) {
16
+ return {
17
+ success: true,
18
+ filePath,
19
+ alreadyInjected: true,
20
+ };
21
+ }
22
+ const snippet = `
23
+ {${INJECTION_MARKER}}
24
+ <iframe
25
+ src="${iframeUrl}"
26
+ style={{
27
+ position: 'fixed',
28
+ bottom: '20px',
29
+ right: '20px',
30
+ width: '400px',
31
+ height: '600px',
32
+ border: 'none',
33
+ borderRadius: '12px',
34
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.25)',
35
+ zIndex: 999999,
36
+ }}
37
+ title="Mayson Dev Overlay"
38
+ />`;
39
+ let updatedContent = null;
40
+ // Option 1: Inject before </body>
41
+ if (/<\/body>/i.test(content)) {
42
+ updatedContent = content.replace(/<\/body>/i, `${snippet}\n </body>`);
43
+ }
44
+ // Option 2: Inject before </html>
45
+ else if (/<\/html>/i.test(content)) {
46
+ updatedContent = content.replace(/<\/html>/i, `${snippet}\n </html>`);
47
+ }
48
+ // Option 3: Inject before the closing tag of the return block (e.g. </main> or </div>)
49
+ else {
50
+ const lastClosingTagIndex = content.lastIndexOf('</');
51
+ if (lastClosingTagIndex !== -1) {
52
+ updatedContent =
53
+ content.slice(0, lastClosingTagIndex) +
54
+ snippet +
55
+ '\n ' +
56
+ content.slice(lastClosingTagIndex);
57
+ }
58
+ }
59
+ if (!updatedContent) {
60
+ return {
61
+ success: false,
62
+ filePath,
63
+ alreadyInjected: false,
64
+ error: 'Could not find suitable JSX insertion point in layout file.',
65
+ };
66
+ }
67
+ if (!dryRun) {
68
+ fs.writeFileSync(filePath, updatedContent, 'utf-8');
69
+ }
70
+ return {
71
+ success: true,
72
+ filePath,
73
+ alreadyInjected: false,
74
+ modifiedContent: updatedContent,
75
+ };
76
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@mayson-org/inject-script",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool and utility to dynamically locate Next.js App Router root layout files and inject the Mayson iframe script/component.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "type": "module",
9
+ "bin": {
10
+ "inject-script": "bin/cli.mjs"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "node --test",
22
+ "prepublishOnly": "npm run build",
23
+ "publish:npm": "npm publish --access public"
24
+ },
25
+ "files": [
26
+ "bin",
27
+ "dist",
28
+ "src",
29
+ "README.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "keywords": [
35
+ "nextjs",
36
+ "app-router",
37
+ "layout",
38
+ "iframe",
39
+ "mayson",
40
+ "cli"
41
+ ],
42
+ "author": "",
43
+ "license": "MIT"
44
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
5
+
6
+ /**
7
+ * Recursively search a directory for layout files.
8
+ */
9
+ function findLayoutFiles(dir: string, fileList: string[] = []): string[] {
10
+ if (!fs.existsSync(dir)) return fileList;
11
+
12
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
13
+
14
+ for (const entry of entries) {
15
+ const fullPath = path.join(dir, entry.name);
16
+
17
+ if (entry.isDirectory()) {
18
+ // Don't traverse node_modules or hidden folders (.next, .git)
19
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
20
+ continue;
21
+ }
22
+ findLayoutFiles(fullPath, fileList);
23
+ } else if (entry.isFile()) {
24
+ const ext = path.extname(entry.name);
25
+ const base = path.basename(entry.name, ext);
26
+ if (base === 'layout' && LAYOUT_EXTENSIONS.includes(ext)) {
27
+ fileList.push(fullPath);
28
+ }
29
+ }
30
+ }
31
+
32
+ return fileList;
33
+ }
34
+
35
+ /**
36
+ * Scores a layout file candidate to determine if it is the root layout.
37
+ * Higher score = higher likelihood of being the root layout.
38
+ */
39
+ function scoreLayoutCandidate(filePath: string, projectRoot: string): number {
40
+ const content = fs.readFileSync(filePath, 'utf-8');
41
+ const relativePath = path.relative(projectRoot, filePath);
42
+ const depth = relativePath.split(path.sep).length;
43
+
44
+ let score = 100 - depth; // Shorter path depth is preferred
45
+
46
+ // Check for HTML/Body root tags
47
+ if (/<html/i.test(content)) score += 50;
48
+ if (/<body/i.test(content)) score += 50;
49
+
50
+ // Check if located directly under app/ or src/app/
51
+ const isDirectAppChild = /^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
52
+ /^app[/\\]layout\.[jt]sx?$/.test(relativePath);
53
+ if (isDirectAppChild) score += 40;
54
+
55
+ return score;
56
+ }
57
+
58
+ /**
59
+ * Finds the most likely Next.js App Router root layout file in a project.
60
+ */
61
+ export function findRootLayout(projectRoot: string = process.cwd()): string | null {
62
+ const possibleAppDirs = [
63
+ path.join(projectRoot, 'src', 'app'),
64
+ path.join(projectRoot, 'app'),
65
+ ];
66
+
67
+ const candidateFiles: string[] = [];
68
+
69
+ for (const appDir of possibleAppDirs) {
70
+ if (fs.existsSync(appDir)) {
71
+ findLayoutFiles(appDir, candidateFiles);
72
+ }
73
+ }
74
+
75
+ if (candidateFiles.length === 0) {
76
+ return null;
77
+ }
78
+
79
+ // Score each candidate to pick the true root layout
80
+ const scored = candidateFiles.map((filePath) => ({
81
+ filePath,
82
+ score: scoreLayoutCandidate(filePath, projectRoot),
83
+ }));
84
+
85
+ scored.sort((a, b) => b.score - a.score);
86
+
87
+ return scored[0]?.filePath || null;
88
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { findRootLayout } from './find-root-layout.js';
2
+ import { injectMaysonIframe, InjectorOptions, InjectorResult } from './injector.js';
3
+
4
+ export { findRootLayout, injectMaysonIframe };
5
+ export type { InjectorOptions, InjectorResult };
6
+
7
+ /**
8
+ * Convenience function to automatically locate and inject the Mayson iframe into a Next.js project.
9
+ */
10
+ export function runAutoInject(projectRoot: string = process.cwd(), iframeUrl: string = 'https://mayson.dev/', dryRun: boolean = false): InjectorResult {
11
+ const layoutPath = findRootLayout(projectRoot);
12
+
13
+ if (!layoutPath) {
14
+ return {
15
+ success: false,
16
+ filePath: '',
17
+ alreadyInjected: false,
18
+ error: `Could not locate Next.js App Router root layout file in ${projectRoot}`,
19
+ };
20
+ }
21
+
22
+ return injectMaysonIframe({
23
+ filePath: layoutPath,
24
+ iframeUrl,
25
+ dryRun,
26
+ });
27
+ }
@@ -0,0 +1,101 @@
1
+ import fs from 'fs';
2
+
3
+ export interface InjectorOptions {
4
+ filePath: string;
5
+ iframeUrl?: string;
6
+ dryRun?: boolean;
7
+ }
8
+
9
+ export interface InjectorResult {
10
+ success: boolean;
11
+ filePath: string;
12
+ alreadyInjected: boolean;
13
+ modifiedContent?: string;
14
+ error?: string;
15
+ }
16
+
17
+ const INJECTION_MARKER = '/* @mayson-iframe-injected */';
18
+
19
+ export function injectMaysonIframe(options: InjectorOptions): InjectorResult {
20
+ const { filePath, iframeUrl = 'https://mayson.dev/', dryRun = false } = options;
21
+
22
+ if (!fs.existsSync(filePath)) {
23
+ return {
24
+ success: false,
25
+ filePath,
26
+ alreadyInjected: false,
27
+ error: `File not found: ${filePath}`,
28
+ };
29
+ }
30
+
31
+ const content = fs.readFileSync(filePath, 'utf-8');
32
+
33
+ // Idempotency check: Don't inject twice
34
+ if (content.includes(INJECTION_MARKER) || content.includes(iframeUrl)) {
35
+ return {
36
+ success: true,
37
+ filePath,
38
+ alreadyInjected: true,
39
+ };
40
+ }
41
+
42
+ const snippet = `
43
+ {${INJECTION_MARKER}}
44
+ <iframe
45
+ src="${iframeUrl}"
46
+ style={{
47
+ position: 'fixed',
48
+ bottom: '20px',
49
+ right: '20px',
50
+ width: '400px',
51
+ height: '600px',
52
+ border: 'none',
53
+ borderRadius: '12px',
54
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.25)',
55
+ zIndex: 999999,
56
+ }}
57
+ title="Mayson Dev Overlay"
58
+ />`;
59
+
60
+ let updatedContent: string | null = null;
61
+
62
+ // Option 1: Inject before </body>
63
+ if (/<\/body>/i.test(content)) {
64
+ updatedContent = content.replace(/<\/body>/i, `${snippet}\n </body>`);
65
+ }
66
+ // Option 2: Inject before </html>
67
+ else if (/<\/html>/i.test(content)) {
68
+ updatedContent = content.replace(/<\/html>/i, `${snippet}\n </html>`);
69
+ }
70
+ // Option 3: Inject before the closing tag of the return block (e.g. </main> or </div>)
71
+ else {
72
+ const lastClosingTagIndex = content.lastIndexOf('</');
73
+ if (lastClosingTagIndex !== -1) {
74
+ updatedContent =
75
+ content.slice(0, lastClosingTagIndex) +
76
+ snippet +
77
+ '\n ' +
78
+ content.slice(lastClosingTagIndex);
79
+ }
80
+ }
81
+
82
+ if (!updatedContent) {
83
+ return {
84
+ success: false,
85
+ filePath,
86
+ alreadyInjected: false,
87
+ error: 'Could not find suitable JSX insertion point in layout file.',
88
+ };
89
+ }
90
+
91
+ if (!dryRun) {
92
+ fs.writeFileSync(filePath, updatedContent, 'utf-8');
93
+ }
94
+
95
+ return {
96
+ success: true,
97
+ filePath,
98
+ alreadyInjected: false,
99
+ modifiedContent: updatedContent,
100
+ };
101
+ }