@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 +71 -0
- package/bin/cli.mjs +159 -0
- package/dist/find-root-layout.d.ts +5 -0
- package/dist/find-root-layout.d.ts.map +1 -0
- package/dist/find-root-layout.js +75 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/injector.d.ts +14 -0
- package/dist/injector.d.ts.map +1 -0
- package/dist/injector.js +76 -0
- package/package.json +44 -0
- package/src/find-root-layout.ts +88 -0
- package/src/index.ts +27 -0
- package/src/injector.ts +101 -0
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 @@
|
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/injector.js
ADDED
|
@@ -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
|
+
}
|
package/src/injector.ts
ADDED
|
@@ -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
|
+
}
|