@mayson-org/inject-script 1.0.0 → 1.0.1
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 +11 -41
- package/bin/cli.mjs +176 -51
- package/dist/find-root-layout.d.ts +0 -3
- package/dist/find-root-layout.d.ts.map +1 -1
- package/dist/find-root-layout.js +1 -15
- package/dist/generators.d.ts +13 -0
- package/dist/generators.d.ts.map +1 -0
- package/dist/generators.js +147 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -8
- package/dist/injector.d.ts +5 -3
- package/dist/injector.d.ts.map +1 -1
- package/dist/injector.js +67 -61
- package/package.json +6 -4
- package/src/find-root-layout.ts +4 -17
- package/src/generators.ts +176 -0
- package/src/index.ts +24 -9
- package/src/injector.ts +73 -66
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @mayson/inject-script
|
|
1
|
+
# @mayson-org/inject-script
|
|
2
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 (
|
|
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 (e.g. `app/layout.tsx`, `src/app/layout.tsx`, or inside route groups like `app/(main)/layout.js`), generates standalone component files (`MaysonBadge.tsx`, `MaysonWatermark.tsx`, `MaysonAscii.tsx`, `MaysonMetadata.tsx`), and injects their imports and JSX tags into the layout tree.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -9,63 +9,33 @@ A lightweight CLI tool and Node.js package designed for **Next.js App Router** a
|
|
|
9
9
|
Run directly in any Next.js project root:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx @mayson/inject-script
|
|
12
|
+
npx @mayson-org/inject-script
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Or pass optional flags:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
# Specify
|
|
19
|
-
npx @mayson/inject-script --
|
|
18
|
+
# Specify component type (badge | watermark | ascii | metadata | all)
|
|
19
|
+
npx @mayson-org/inject-script --type=badge
|
|
20
20
|
|
|
21
21
|
# Dry-run mode (preview changes without modifying files)
|
|
22
|
-
npx @mayson/inject-script --dry-run
|
|
22
|
+
npx @mayson-org/inject-script --dry-run
|
|
23
23
|
|
|
24
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
|
-
}
|
|
25
|
+
npx @mayson-org/inject-script --dir=./my-nextjs-app
|
|
42
26
|
```
|
|
43
27
|
|
|
44
28
|
---
|
|
45
29
|
|
|
46
30
|
## Programmatic API Usage
|
|
47
31
|
|
|
48
|
-
You can also import and use `inject-script` in custom build scripts:
|
|
49
|
-
|
|
50
32
|
```javascript
|
|
51
|
-
import { runAutoInject, findRootLayout,
|
|
33
|
+
import { runAutoInject, findRootLayout, generateComponents, injectComponentsIntoLayout } from '@mayson-org/inject-script';
|
|
52
34
|
|
|
53
|
-
//
|
|
54
|
-
const result = runAutoInject(process.cwd(), '
|
|
35
|
+
// Auto-detect layout, generate 'badge' component file, and inject into layout
|
|
36
|
+
const result = runAutoInject(process.cwd(), ['badge', 'watermark']);
|
|
55
37
|
|
|
56
38
|
if (result.success) {
|
|
57
|
-
console.log(
|
|
39
|
+
console.log('Injected components:', result.injectedComponents);
|
|
58
40
|
}
|
|
59
|
-
|
|
60
|
-
// 2. Or manually find layout and inspect
|
|
61
|
-
const rootLayoutPath = findRootLayout('./my-project');
|
|
62
|
-
console.log('Root Layout found at:', rootLayoutPath);
|
|
63
41
|
```
|
|
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
CHANGED
|
@@ -50,11 +50,115 @@ function findRootLayout(projectRoot) {
|
|
|
50
50
|
return candidates[0];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
const TEMPLATES = {
|
|
54
|
+
badge: {
|
|
55
|
+
name: 'MaysonBadge',
|
|
56
|
+
code: `'use client';
|
|
57
|
+
import React from 'react';
|
|
58
|
+
|
|
59
|
+
export function MaysonBadge() {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
position: 'fixed',
|
|
64
|
+
bottom: '16px',
|
|
65
|
+
right: '16px',
|
|
66
|
+
zIndex: 99999,
|
|
67
|
+
display: 'flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
gap: '8px',
|
|
70
|
+
padding: '6px 12px',
|
|
71
|
+
backgroundColor: 'rgba(18, 18, 20, 0.9)',
|
|
72
|
+
color: '#ffffff',
|
|
73
|
+
borderRadius: '9999px',
|
|
74
|
+
fontSize: '12px',
|
|
75
|
+
fontWeight: 500,
|
|
76
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
77
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
78
|
+
boxShadow: '0 4px 14px rgba(0, 0, 0, 0.25)',
|
|
79
|
+
backdropFilter: 'blur(8px)',
|
|
80
|
+
cursor: 'pointer',
|
|
81
|
+
userSelect: 'none',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#6366f1' }} />
|
|
85
|
+
<span>Built with <strong>Mayson</strong></span>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
`,
|
|
90
|
+
},
|
|
91
|
+
watermark: {
|
|
92
|
+
name: 'MaysonWatermark',
|
|
93
|
+
code: `'use client';
|
|
94
|
+
import React from 'react';
|
|
95
|
+
|
|
96
|
+
export function MaysonWatermark() {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
position: 'fixed',
|
|
101
|
+
bottom: '12px',
|
|
102
|
+
left: '12px',
|
|
103
|
+
zIndex: 99999,
|
|
104
|
+
opacity: 0.4,
|
|
105
|
+
fontSize: '11px',
|
|
106
|
+
fontFamily: 'monospace',
|
|
107
|
+
color: '#888888',
|
|
108
|
+
pointerEvents: 'none',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
⚡ Mayson Watermark
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
ascii: {
|
|
118
|
+
name: 'MaysonAscii',
|
|
119
|
+
code: `'use client';
|
|
120
|
+
import React from 'react';
|
|
121
|
+
|
|
122
|
+
export function MaysonAscii() {
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
position: 'fixed',
|
|
127
|
+
top: '8px',
|
|
128
|
+
right: '12px',
|
|
129
|
+
zIndex: 99999,
|
|
130
|
+
fontSize: '10px',
|
|
131
|
+
fontFamily: 'monospace',
|
|
132
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
133
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
134
|
+
padding: '4px 8px',
|
|
135
|
+
borderRadius: '4px',
|
|
136
|
+
pointerEvents: 'none',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
MAYSON :: RUNTIME ACTIVE
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
`,
|
|
144
|
+
},
|
|
145
|
+
metadata: {
|
|
146
|
+
name: 'MaysonMetadata',
|
|
147
|
+
code: `'use client';
|
|
148
|
+
import React from 'react';
|
|
149
|
+
|
|
150
|
+
export function MaysonMetadata() {
|
|
151
|
+
return <meta name="mayson-generator" content="Mayson App Platform" />;
|
|
152
|
+
}
|
|
153
|
+
`,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
53
157
|
function parseArgs() {
|
|
54
158
|
const args = process.argv.slice(2);
|
|
55
159
|
const options = {
|
|
56
160
|
projectRoot: process.cwd(),
|
|
57
|
-
|
|
161
|
+
type: 'badge',
|
|
58
162
|
dryRun: false,
|
|
59
163
|
help: false,
|
|
60
164
|
};
|
|
@@ -65,10 +169,10 @@ function parseArgs() {
|
|
|
65
169
|
options.help = true;
|
|
66
170
|
} else if (arg === '--dry-run') {
|
|
67
171
|
options.dryRun = true;
|
|
68
|
-
} else if (arg.startsWith('--
|
|
69
|
-
options.
|
|
70
|
-
} else if (arg === '--
|
|
71
|
-
options.
|
|
172
|
+
} else if (arg.startsWith('--type=')) {
|
|
173
|
+
options.type = arg.split('=')[1];
|
|
174
|
+
} else if (arg === '--type' && args[i + 1]) {
|
|
175
|
+
options.type = args[++i];
|
|
72
176
|
} else if (arg.startsWith('--dir=')) {
|
|
73
177
|
options.projectRoot = path.resolve(arg.split('=')[1]);
|
|
74
178
|
} else if (arg === '--dir' && args[i + 1]) {
|
|
@@ -82,15 +186,15 @@ const options = parseArgs();
|
|
|
82
186
|
|
|
83
187
|
if (options.help) {
|
|
84
188
|
console.log(`
|
|
85
|
-
|
|
189
|
+
@mayson-org/inject-script CLI - Generate & Inject Mayson Components into Next.js App Router
|
|
86
190
|
|
|
87
191
|
Usage:
|
|
88
|
-
npx inject-script [options]
|
|
192
|
+
npx @mayson-org/inject-script [options]
|
|
89
193
|
|
|
90
194
|
Options:
|
|
91
195
|
--dir <path> Target project root directory (default: current working directory)
|
|
92
|
-
--
|
|
93
|
-
--dry-run Simulate
|
|
196
|
+
--type <type> Component type to inject: badge | watermark | ascii | metadata | all (default: badge)
|
|
197
|
+
--dry-run Simulate component generation and layout injection without modifying files
|
|
94
198
|
--help, -h Show this help message
|
|
95
199
|
`);
|
|
96
200
|
process.exit(0);
|
|
@@ -107,53 +211,74 @@ if (!layoutFile) {
|
|
|
107
211
|
|
|
108
212
|
console.log(`📍 Found Root Layout: ${path.relative(options.projectRoot, layoutFile)}`);
|
|
109
213
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
214
|
+
const layoutDir = path.dirname(layoutFile);
|
|
215
|
+
const isTypeScript = layoutFile.endsWith('.tsx') || layoutFile.endsWith('.ts');
|
|
216
|
+
const ext = isTypeScript ? '.tsx' : '.jsx';
|
|
112
217
|
|
|
113
|
-
|
|
114
|
-
console.log(`✅ Mayson iframe is already injected in ${path.basename(layoutFile)}.`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
218
|
+
const selectedTypes = options.type === 'all' ? ['badge', 'watermark', 'ascii', 'metadata'] : [options.type];
|
|
117
219
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
220
|
+
let content = fs.readFileSync(layoutFile, 'utf-8');
|
|
221
|
+
const injectedNames = [];
|
|
222
|
+
|
|
223
|
+
for (const t of selectedTypes) {
|
|
224
|
+
const template = TEMPLATES[t];
|
|
225
|
+
if (!template) {
|
|
226
|
+
console.warn(`⚠️ Unknown component type '${t}'. Skipping.`);
|
|
227
|
+
continue;
|
|
146
228
|
}
|
|
147
|
-
}
|
|
148
229
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
230
|
+
const { name, code } = template;
|
|
231
|
+
const compPath = path.join(layoutDir, `${name}${ext}`);
|
|
232
|
+
|
|
233
|
+
// Create Component File
|
|
234
|
+
if (options.dryRun) {
|
|
235
|
+
console.log(`🧪 [Dry Run] Would create component file: ${path.relative(options.projectRoot, compPath)}`);
|
|
236
|
+
} else {
|
|
237
|
+
fs.writeFileSync(compPath, code, 'utf-8');
|
|
238
|
+
console.log(`✨ Created Component File: ${path.relative(options.projectRoot, compPath)}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if component already present in layout
|
|
242
|
+
if (content.includes(name)) {
|
|
243
|
+
console.log(`ℹ️ Component ${name} is already present in ${path.basename(layoutFile)}.`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Insert import statement
|
|
248
|
+
const importLine = `import { ${name} } from './${name}';\n`;
|
|
249
|
+
if (/import\s+.*?from\s+['"].*?['"];?/g.test(content)) {
|
|
250
|
+
const matches = Array.from(content.matchAll(/import\s+.*?from\s+['"].*?['"];?/g));
|
|
251
|
+
const last = matches[matches.length - 1];
|
|
252
|
+
const idx = last.index + last[0].length;
|
|
253
|
+
content = content.slice(0, idx) + '\n' + importLine + content.slice(idx);
|
|
254
|
+
} else {
|
|
255
|
+
content = importLine + content;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Insert JSX snippet
|
|
259
|
+
const snippet = `\n {/* @mayson-component-injected: ${name} */}\n <${name} />`;
|
|
260
|
+
|
|
261
|
+
if (/<\/body>/i.test(content)) {
|
|
262
|
+
content = content.replace(/<\/body>/i, `${snippet}\n </body>`);
|
|
263
|
+
} else if (/<\/html>/i.test(content)) {
|
|
264
|
+
content = content.replace(/<\/html>/i, `${snippet}\n </html>`);
|
|
265
|
+
} else {
|
|
266
|
+
const lastIdx = content.lastIndexOf('</');
|
|
267
|
+
if (lastIdx !== -1) {
|
|
268
|
+
content = content.slice(0, lastIdx) + snippet + '\n ' + content.slice(lastIdx);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
injectedNames.push(name);
|
|
152
273
|
}
|
|
153
274
|
|
|
154
|
-
if (
|
|
155
|
-
|
|
275
|
+
if (injectedNames.length > 0) {
|
|
276
|
+
if (options.dryRun) {
|
|
277
|
+
console.log(`🧪 [Dry Run] Would inject components into ${path.relative(options.projectRoot, layoutFile)}: ${injectedNames.join(', ')}`);
|
|
278
|
+
} else {
|
|
279
|
+
fs.writeFileSync(layoutFile, content, 'utf-8');
|
|
280
|
+
console.log(`🎉 Injected components into ${path.relative(options.projectRoot, layoutFile)}: ${injectedNames.join(', ')}`);
|
|
281
|
+
}
|
|
156
282
|
} else {
|
|
157
|
-
|
|
158
|
-
console.log(`🎉 Successfully injected Mayson iframe (${options.url}) into ${path.relative(options.projectRoot, layoutFile)}!`);
|
|
283
|
+
console.log(`✅ Root layout is up to date.`);
|
|
159
284
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-root-layout.d.ts","sourceRoot":"","sources":["../src/find-root-layout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"find-root-layout.d.ts","sourceRoot":"","sources":["../src/find-root-layout.ts"],"names":[],"mappings":"AAgDA,wBAAgB,cAAc,CAAC,WAAW,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA0BjF"}
|
package/dist/find-root-layout.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
4
|
-
/**
|
|
5
|
-
* Recursively search a directory for layout files.
|
|
6
|
-
*/
|
|
7
4
|
function findLayoutFiles(dir, fileList = []) {
|
|
8
5
|
if (!fs.existsSync(dir))
|
|
9
6
|
return fileList;
|
|
@@ -11,7 +8,6 @@ function findLayoutFiles(dir, fileList = []) {
|
|
|
11
8
|
for (const entry of entries) {
|
|
12
9
|
const fullPath = path.join(dir, entry.name);
|
|
13
10
|
if (entry.isDirectory()) {
|
|
14
|
-
// Don't traverse node_modules or hidden folders (.next, .git)
|
|
15
11
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
16
12
|
continue;
|
|
17
13
|
}
|
|
@@ -27,30 +23,21 @@ function findLayoutFiles(dir, fileList = []) {
|
|
|
27
23
|
}
|
|
28
24
|
return fileList;
|
|
29
25
|
}
|
|
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
26
|
function scoreLayoutCandidate(filePath, projectRoot) {
|
|
35
27
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
36
28
|
const relativePath = path.relative(projectRoot, filePath);
|
|
37
29
|
const depth = relativePath.split(path.sep).length;
|
|
38
|
-
let score = 100 - depth;
|
|
39
|
-
// Check for HTML/Body root tags
|
|
30
|
+
let score = 100 - depth;
|
|
40
31
|
if (/<html/i.test(content))
|
|
41
32
|
score += 50;
|
|
42
33
|
if (/<body/i.test(content))
|
|
43
34
|
score += 50;
|
|
44
|
-
// Check if located directly under app/ or src/app/
|
|
45
35
|
const isDirectAppChild = /^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
|
|
46
36
|
/^app[/\\]layout\.[jt]sx?$/.test(relativePath);
|
|
47
37
|
if (isDirectAppChild)
|
|
48
38
|
score += 40;
|
|
49
39
|
return score;
|
|
50
40
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Finds the most likely Next.js App Router root layout file in a project.
|
|
53
|
-
*/
|
|
54
41
|
export function findRootLayout(projectRoot = process.cwd()) {
|
|
55
42
|
const possibleAppDirs = [
|
|
56
43
|
path.join(projectRoot, 'src', 'app'),
|
|
@@ -65,7 +52,6 @@ export function findRootLayout(projectRoot = process.cwd()) {
|
|
|
65
52
|
if (candidateFiles.length === 0) {
|
|
66
53
|
return null;
|
|
67
54
|
}
|
|
68
|
-
// Score each candidate to pick the true root layout
|
|
69
55
|
const scored = candidateFiles.map((filePath) => ({
|
|
70
56
|
filePath,
|
|
71
57
|
score: scoreLayoutCandidate(filePath, projectRoot),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ComponentType = 'badge' | 'watermark' | 'ascii' | 'metadata' | 'all';
|
|
2
|
+
export interface ComponentGeneratorOptions {
|
|
3
|
+
outputDir: string;
|
|
4
|
+
isTypeScript?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface GeneratedComponentInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
filePath: string;
|
|
9
|
+
importStatement: string;
|
|
10
|
+
jsxTag: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function generateComponents(types: ComponentType[] | undefined, options: ComponentGeneratorOptions): GeneratedComponentInfo[];
|
|
13
|
+
//# sourceMappingURL=generators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../src/generators.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;AAEjF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AA2GD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,aAAa,EAAE,YAAY,EAClC,OAAO,EAAE,yBAAyB,GACjC,sBAAsB,EAAE,CAkD1B"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const BADGE_TEMPLATE = `'use client';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
export function MaysonBadge() {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
style={{
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
bottom: '16px',
|
|
12
|
+
right: '16px',
|
|
13
|
+
zIndex: 99999,
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
gap: '8px',
|
|
17
|
+
padding: '6px 12px',
|
|
18
|
+
backgroundColor: 'rgba(18, 18, 20, 0.9)',
|
|
19
|
+
color: '#ffffff',
|
|
20
|
+
borderRadius: '9999px',
|
|
21
|
+
fontSize: '12px',
|
|
22
|
+
fontWeight: 500,
|
|
23
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
24
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
25
|
+
boxShadow: '0 4px 14px rgba(0, 0, 0, 0.25)',
|
|
26
|
+
backdropFilter: 'blur(8px)',
|
|
27
|
+
cursor: 'pointer',
|
|
28
|
+
userSelect: 'none',
|
|
29
|
+
transition: 'transform 0.2s ease',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#6366f1' }} />
|
|
33
|
+
<span>Built with <strong>Mayson</strong></span>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const WATERMARK_TEMPLATE = `'use client';
|
|
39
|
+
import React from 'react';
|
|
40
|
+
|
|
41
|
+
export function MaysonWatermark() {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
position: 'fixed',
|
|
46
|
+
bottom: '12px',
|
|
47
|
+
left: '12px',
|
|
48
|
+
zIndex: 99999,
|
|
49
|
+
opacity: 0.4,
|
|
50
|
+
fontSize: '11px',
|
|
51
|
+
fontFamily: 'monospace',
|
|
52
|
+
color: '#888888',
|
|
53
|
+
pointerEvents: 'none',
|
|
54
|
+
userSelect: 'none',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
⚡ Mayson Watermark
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const ASCII_TEMPLATE = `'use client';
|
|
63
|
+
import React from 'react';
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
███╗ ███╗██xA1██╗ ██╗███████╗██████╗ ██████╗
|
|
67
|
+
████╗ ████║██║██║ ██║██╔════╝██╔═══██╗██╔══██╗
|
|
68
|
+
██╔████╔██║██║██║ ██║███████╗██║ ██║██║ ██║
|
|
69
|
+
██║╚██╔╝██║██║██║ ██║╚════██║██║ ██║██║ ██║
|
|
70
|
+
██║ ╚═╝ ██║██║╚██████╔╝███████║╚██████╔╝██████╔╝
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
export function MaysonAscii() {
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
position: 'fixed',
|
|
78
|
+
top: '8px',
|
|
79
|
+
right: '12px',
|
|
80
|
+
zIndex: 99999,
|
|
81
|
+
fontSize: '10px',
|
|
82
|
+
fontFamily: 'monospace',
|
|
83
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
84
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
85
|
+
padding: '4px 8px',
|
|
86
|
+
borderRadius: '4px',
|
|
87
|
+
pointerEvents: 'none',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
MAYSON :: RUNTIME ACTIVE
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
const METADATA_TEMPLATE = `'use client';
|
|
96
|
+
import React from 'react';
|
|
97
|
+
|
|
98
|
+
export function MaysonMetadata() {
|
|
99
|
+
return (
|
|
100
|
+
<meta name="mayson-generator" content="Mayson App Platform" />
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
export function generateComponents(types = ['badge'], options) {
|
|
105
|
+
const { outputDir, isTypeScript = true } = options;
|
|
106
|
+
const ext = isTypeScript ? '.tsx' : '.jsx';
|
|
107
|
+
const results = [];
|
|
108
|
+
if (!fs.existsSync(outputDir)) {
|
|
109
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
const selectedTypes = types.includes('all')
|
|
112
|
+
? ['badge', 'watermark', 'ascii', 'metadata']
|
|
113
|
+
: types;
|
|
114
|
+
for (const type of selectedTypes) {
|
|
115
|
+
let name = '';
|
|
116
|
+
let code = '';
|
|
117
|
+
switch (type) {
|
|
118
|
+
case 'badge':
|
|
119
|
+
name = 'MaysonBadge';
|
|
120
|
+
code = BADGE_TEMPLATE;
|
|
121
|
+
break;
|
|
122
|
+
case 'watermark':
|
|
123
|
+
name = 'MaysonWatermark';
|
|
124
|
+
code = WATERMARK_TEMPLATE;
|
|
125
|
+
break;
|
|
126
|
+
case 'ascii':
|
|
127
|
+
name = 'MaysonAscii';
|
|
128
|
+
code = ASCII_TEMPLATE;
|
|
129
|
+
break;
|
|
130
|
+
case 'metadata':
|
|
131
|
+
name = 'MaysonMetadata';
|
|
132
|
+
code = METADATA_TEMPLATE;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (name && code) {
|
|
136
|
+
const filePath = path.join(outputDir, `${name}${ext}`);
|
|
137
|
+
fs.writeFileSync(filePath, code, 'utf-8');
|
|
138
|
+
results.push({
|
|
139
|
+
name,
|
|
140
|
+
filePath,
|
|
141
|
+
importStatement: `import { ${name} } from './${name}';`,
|
|
142
|
+
jsxTag: `<${name} />`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { findRootLayout } from './find-root-layout.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export
|
|
2
|
+
import { generateComponents, ComponentType, GeneratedComponentInfo } from './generators.js';
|
|
3
|
+
import { injectComponentsIntoLayout, InjectorOptions, InjectorResult } from './injector.js';
|
|
4
|
+
export { findRootLayout, generateComponents, injectComponentsIntoLayout };
|
|
5
|
+
export type { ComponentType, GeneratedComponentInfo, InjectorOptions, InjectorResult };
|
|
5
6
|
/**
|
|
6
|
-
* Convenience function to
|
|
7
|
+
* Convenience function to auto-detect Next.js App Router root layout, generate component files, and inject them.
|
|
7
8
|
*/
|
|
8
|
-
export declare function runAutoInject(projectRoot?: string,
|
|
9
|
+
export declare function runAutoInject(projectRoot?: string, types?: ComponentType[], dryRun?: boolean): InjectorResult;
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE5F,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,CAAC;AAC1E,YAAY,EAAE,aAAa,EAAE,sBAAsB,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAEvF;;GAEG;AACH,wBAAgB,aAAa,CAC3B,WAAW,GAAE,MAAsB,EACnC,KAAK,GAAE,aAAa,EAAc,EAClC,MAAM,GAAE,OAAe,GACtB,cAAc,CA0BhB"}
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { findRootLayout } from './find-root-layout.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import { generateComponents } from './generators.js';
|
|
4
|
+
import { injectComponentsIntoLayout } from './injector.js';
|
|
5
|
+
export { findRootLayout, generateComponents, injectComponentsIntoLayout };
|
|
4
6
|
/**
|
|
5
|
-
* Convenience function to
|
|
7
|
+
* Convenience function to auto-detect Next.js App Router root layout, generate component files, and inject them.
|
|
6
8
|
*/
|
|
7
|
-
export function runAutoInject(projectRoot = process.cwd(),
|
|
9
|
+
export function runAutoInject(projectRoot = process.cwd(), types = ['badge'], dryRun = false) {
|
|
8
10
|
const layoutPath = findRootLayout(projectRoot);
|
|
9
11
|
if (!layoutPath) {
|
|
10
12
|
return {
|
|
11
13
|
success: false,
|
|
12
14
|
filePath: '',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
injectedComponents: [],
|
|
16
|
+
skippedComponents: [],
|
|
17
|
+
error: `Could not locate Next.js App Router root layout in ${projectRoot}`,
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const isTypeScript = layoutPath.endsWith('.tsx') || layoutPath.endsWith('.ts');
|
|
21
|
+
const targetDir = path.dirname(layoutPath);
|
|
22
|
+
const generatedComponents = generateComponents(types, {
|
|
23
|
+
outputDir: targetDir,
|
|
24
|
+
isTypeScript,
|
|
25
|
+
});
|
|
26
|
+
return injectComponentsIntoLayout({
|
|
18
27
|
filePath: layoutPath,
|
|
19
|
-
|
|
28
|
+
components: generatedComponents,
|
|
20
29
|
dryRun,
|
|
21
30
|
});
|
|
22
31
|
}
|
package/dist/injector.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { GeneratedComponentInfo } from './generators.js';
|
|
1
2
|
export interface InjectorOptions {
|
|
2
3
|
filePath: string;
|
|
3
|
-
|
|
4
|
+
components: GeneratedComponentInfo[];
|
|
4
5
|
dryRun?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export interface InjectorResult {
|
|
7
8
|
success: boolean;
|
|
8
9
|
filePath: string;
|
|
9
|
-
|
|
10
|
+
injectedComponents: string[];
|
|
11
|
+
skippedComponents: string[];
|
|
10
12
|
modifiedContent?: string;
|
|
11
13
|
error?: string;
|
|
12
14
|
}
|
|
13
|
-
export declare function
|
|
15
|
+
export declare function injectComponentsIntoLayout(options: InjectorOptions): InjectorResult;
|
|
14
16
|
//# sourceMappingURL=injector.d.ts.map
|
package/dist/injector.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"injector.d.ts","sourceRoot":"","sources":["../src/injector.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CAwFnF"}
|
package/dist/injector.js
CHANGED
|
@@ -1,76 +1,82 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
export function
|
|
4
|
-
const { filePath,
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function injectComponentsIntoLayout(options) {
|
|
4
|
+
const { filePath, components, dryRun = false } = options;
|
|
5
5
|
if (!fs.existsSync(filePath)) {
|
|
6
6
|
return {
|
|
7
7
|
success: false,
|
|
8
8
|
filePath,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
injectedComponents: [],
|
|
10
|
+
skippedComponents: [],
|
|
11
|
+
error: `Layout file not found: ${filePath}`,
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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);
|
|
14
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
15
|
+
const injectedComponents = [];
|
|
16
|
+
const skippedComponents = [];
|
|
17
|
+
for (const comp of components) {
|
|
18
|
+
const importMarker = comp.name;
|
|
19
|
+
const jsxMarker = `<${comp.name}`;
|
|
20
|
+
// Skip if already injected
|
|
21
|
+
if (content.includes(importMarker) || content.includes(jsxMarker)) {
|
|
22
|
+
skippedComponents.push(comp.name);
|
|
23
|
+
continue;
|
|
57
24
|
}
|
|
25
|
+
// Determine component file import path relative to layout.tsx
|
|
26
|
+
const layoutDir = path.dirname(filePath);
|
|
27
|
+
const compDir = path.dirname(comp.filePath);
|
|
28
|
+
const compBaseName = path.basename(comp.filePath, path.extname(comp.filePath));
|
|
29
|
+
let relativeImportPath = path.relative(layoutDir, path.join(compDir, compBaseName));
|
|
30
|
+
if (!relativeImportPath.startsWith('.')) {
|
|
31
|
+
relativeImportPath = `./${relativeImportPath}`;
|
|
32
|
+
}
|
|
33
|
+
// Convert backslashes for Windows path consistency
|
|
34
|
+
relativeImportPath = relativeImportPath.replace(/\\/g, '/');
|
|
35
|
+
const importLine = `import { ${comp.name} } from '${relativeImportPath}';\n`;
|
|
36
|
+
// 1. Insert import statement
|
|
37
|
+
if (/import\s+.*?from\s+['"].*?['"];?/g.test(content)) {
|
|
38
|
+
// Insert after the last import statement
|
|
39
|
+
const importMatches = Array.from(content.matchAll(/import\s+.*?from\s+['"].*?['"];?/g));
|
|
40
|
+
const lastMatch = importMatches[importMatches.length - 1];
|
|
41
|
+
if (lastMatch && lastMatch.index !== undefined) {
|
|
42
|
+
const insertPos = lastMatch.index + lastMatch[0].length;
|
|
43
|
+
content = content.slice(0, insertPos) + '\n' + importLine + content.slice(insertPos);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
content = importLine + content;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
content = importLine + content;
|
|
51
|
+
}
|
|
52
|
+
// 2. Insert JSX tag into return block
|
|
53
|
+
const jsxSnippet = `\n {/* @mayson-component-injected: ${comp.name} */}\n ${comp.jsxTag}`;
|
|
54
|
+
if (/<\/body>/i.test(content)) {
|
|
55
|
+
content = content.replace(/<\/body>/i, `${jsxSnippet}\n </body>`);
|
|
56
|
+
}
|
|
57
|
+
else if (/<\/html>/i.test(content)) {
|
|
58
|
+
content = content.replace(/<\/html>/i, `${jsxSnippet}\n </html>`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const lastClosingIndex = content.lastIndexOf('</');
|
|
62
|
+
if (lastClosingIndex !== -1) {
|
|
63
|
+
content =
|
|
64
|
+
content.slice(0, lastClosingIndex) +
|
|
65
|
+
jsxSnippet +
|
|
66
|
+
'\n ' +
|
|
67
|
+
content.slice(lastClosingIndex);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
injectedComponents.push(comp.name);
|
|
58
71
|
}
|
|
59
|
-
if (!
|
|
60
|
-
|
|
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');
|
|
72
|
+
if (injectedComponents.length > 0 && !dryRun) {
|
|
73
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
69
74
|
}
|
|
70
75
|
return {
|
|
71
76
|
success: true,
|
|
72
77
|
filePath,
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
injectedComponents,
|
|
79
|
+
skippedComponents,
|
|
80
|
+
modifiedContent: content,
|
|
75
81
|
};
|
|
76
82
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mayson-org/inject-script",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI tool and utility to dynamically locate Next.js App Router root layout files and inject
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI tool and utility to dynamically locate Next.js App Router root layout files, generate component files (Badge, Watermark, ASCII, Metadata), and inject their imports.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -35,10 +35,12 @@
|
|
|
35
35
|
"nextjs",
|
|
36
36
|
"app-router",
|
|
37
37
|
"layout",
|
|
38
|
-
"
|
|
38
|
+
"badge",
|
|
39
|
+
"watermark",
|
|
40
|
+
"metadata",
|
|
39
41
|
"mayson",
|
|
40
42
|
"cli"
|
|
41
43
|
],
|
|
42
44
|
"author": "",
|
|
43
45
|
"license": "MIT"
|
|
44
|
-
}
|
|
46
|
+
}
|
package/src/find-root-layout.ts
CHANGED
|
@@ -3,9 +3,6 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Recursively search a directory for layout files.
|
|
8
|
-
*/
|
|
9
6
|
function findLayoutFiles(dir: string, fileList: string[] = []): string[] {
|
|
10
7
|
if (!fs.existsSync(dir)) return fileList;
|
|
11
8
|
|
|
@@ -15,7 +12,6 @@ function findLayoutFiles(dir: string, fileList: string[] = []): string[] {
|
|
|
15
12
|
const fullPath = path.join(dir, entry.name);
|
|
16
13
|
|
|
17
14
|
if (entry.isDirectory()) {
|
|
18
|
-
// Don't traverse node_modules or hidden folders (.next, .git)
|
|
19
15
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
20
16
|
continue;
|
|
21
17
|
}
|
|
@@ -32,32 +28,24 @@ function findLayoutFiles(dir: string, fileList: string[] = []): string[] {
|
|
|
32
28
|
return fileList;
|
|
33
29
|
}
|
|
34
30
|
|
|
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
31
|
function scoreLayoutCandidate(filePath: string, projectRoot: string): number {
|
|
40
32
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
33
|
const relativePath = path.relative(projectRoot, filePath);
|
|
42
34
|
const depth = relativePath.split(path.sep).length;
|
|
43
35
|
|
|
44
|
-
let score = 100 - depth;
|
|
36
|
+
let score = 100 - depth;
|
|
45
37
|
|
|
46
|
-
// Check for HTML/Body root tags
|
|
47
38
|
if (/<html/i.test(content)) score += 50;
|
|
48
39
|
if (/<body/i.test(content)) score += 50;
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
const isDirectAppChild =
|
|
42
|
+
/^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
|
|
43
|
+
/^app[/\\]layout\.[jt]sx?$/.test(relativePath);
|
|
53
44
|
if (isDirectAppChild) score += 40;
|
|
54
45
|
|
|
55
46
|
return score;
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
/**
|
|
59
|
-
* Finds the most likely Next.js App Router root layout file in a project.
|
|
60
|
-
*/
|
|
61
49
|
export function findRootLayout(projectRoot: string = process.cwd()): string | null {
|
|
62
50
|
const possibleAppDirs = [
|
|
63
51
|
path.join(projectRoot, 'src', 'app'),
|
|
@@ -76,7 +64,6 @@ export function findRootLayout(projectRoot: string = process.cwd()): string | nu
|
|
|
76
64
|
return null;
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
// Score each candidate to pick the true root layout
|
|
80
67
|
const scored = candidateFiles.map((filePath) => ({
|
|
81
68
|
filePath,
|
|
82
69
|
score: scoreLayoutCandidate(filePath, projectRoot),
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export type ComponentType = 'badge' | 'watermark' | 'ascii' | 'metadata' | 'all';
|
|
5
|
+
|
|
6
|
+
export interface ComponentGeneratorOptions {
|
|
7
|
+
outputDir: string;
|
|
8
|
+
isTypeScript?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GeneratedComponentInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
filePath: string;
|
|
14
|
+
importStatement: string;
|
|
15
|
+
jsxTag: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BADGE_TEMPLATE = `'use client';
|
|
19
|
+
import React from 'react';
|
|
20
|
+
|
|
21
|
+
export function MaysonBadge() {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
style={{
|
|
25
|
+
position: 'fixed',
|
|
26
|
+
bottom: '16px',
|
|
27
|
+
right: '16px',
|
|
28
|
+
zIndex: 99999,
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
gap: '8px',
|
|
32
|
+
padding: '6px 12px',
|
|
33
|
+
backgroundColor: 'rgba(18, 18, 20, 0.9)',
|
|
34
|
+
color: '#ffffff',
|
|
35
|
+
borderRadius: '9999px',
|
|
36
|
+
fontSize: '12px',
|
|
37
|
+
fontWeight: 500,
|
|
38
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
39
|
+
border: '1px solid rgba(255, 255, 255, 0.15)',
|
|
40
|
+
boxShadow: '0 4px 14px rgba(0, 0, 0, 0.25)',
|
|
41
|
+
backdropFilter: 'blur(8px)',
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
userSelect: 'none',
|
|
44
|
+
transition: 'transform 0.2s ease',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#6366f1' }} />
|
|
48
|
+
<span>Built with <strong>Mayson</strong></span>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const WATERMARK_TEMPLATE = `'use client';
|
|
55
|
+
import React from 'react';
|
|
56
|
+
|
|
57
|
+
export function MaysonWatermark() {
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
style={{
|
|
61
|
+
position: 'fixed',
|
|
62
|
+
bottom: '12px',
|
|
63
|
+
left: '12px',
|
|
64
|
+
zIndex: 99999,
|
|
65
|
+
opacity: 0.4,
|
|
66
|
+
fontSize: '11px',
|
|
67
|
+
fontFamily: 'monospace',
|
|
68
|
+
color: '#888888',
|
|
69
|
+
pointerEvents: 'none',
|
|
70
|
+
userSelect: 'none',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
⚡ Mayson Watermark
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const ASCII_TEMPLATE = `'use client';
|
|
80
|
+
import React from 'react';
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
███╗ ███╗██xA1██╗ ██╗███████╗██████╗ ██████╗
|
|
84
|
+
████╗ ████║██║██║ ██║██╔════╝██╔═══██╗██╔══██╗
|
|
85
|
+
██╔████╔██║██║██║ ██║███████╗██║ ██║██║ ██║
|
|
86
|
+
██║╚██╔╝██║██║██║ ██║╚════██║██║ ██║██║ ██║
|
|
87
|
+
██║ ╚═╝ ██║██║╚██████╔╝███████║╚██████╔╝██████╔╝
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
export function MaysonAscii() {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
position: 'fixed',
|
|
95
|
+
top: '8px',
|
|
96
|
+
right: '12px',
|
|
97
|
+
zIndex: 99999,
|
|
98
|
+
fontSize: '10px',
|
|
99
|
+
fontFamily: 'monospace',
|
|
100
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
101
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
102
|
+
padding: '4px 8px',
|
|
103
|
+
borderRadius: '4px',
|
|
104
|
+
pointerEvents: 'none',
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
MAYSON :: RUNTIME ACTIVE
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
const METADATA_TEMPLATE = `'use client';
|
|
114
|
+
import React from 'react';
|
|
115
|
+
|
|
116
|
+
export function MaysonMetadata() {
|
|
117
|
+
return (
|
|
118
|
+
<meta name="mayson-generator" content="Mayson App Platform" />
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
export function generateComponents(
|
|
124
|
+
types: ComponentType[] = ['badge'],
|
|
125
|
+
options: ComponentGeneratorOptions
|
|
126
|
+
): GeneratedComponentInfo[] {
|
|
127
|
+
const { outputDir, isTypeScript = true } = options;
|
|
128
|
+
const ext = isTypeScript ? '.tsx' : '.jsx';
|
|
129
|
+
const results: GeneratedComponentInfo[] = [];
|
|
130
|
+
|
|
131
|
+
if (!fs.existsSync(outputDir)) {
|
|
132
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const selectedTypes = types.includes('all')
|
|
136
|
+
? (['badge', 'watermark', 'ascii', 'metadata'] as ComponentType[])
|
|
137
|
+
: types;
|
|
138
|
+
|
|
139
|
+
for (const type of selectedTypes) {
|
|
140
|
+
let name = '';
|
|
141
|
+
let code = '';
|
|
142
|
+
|
|
143
|
+
switch (type) {
|
|
144
|
+
case 'badge':
|
|
145
|
+
name = 'MaysonBadge';
|
|
146
|
+
code = BADGE_TEMPLATE;
|
|
147
|
+
break;
|
|
148
|
+
case 'watermark':
|
|
149
|
+
name = 'MaysonWatermark';
|
|
150
|
+
code = WATERMARK_TEMPLATE;
|
|
151
|
+
break;
|
|
152
|
+
case 'ascii':
|
|
153
|
+
name = 'MaysonAscii';
|
|
154
|
+
code = ASCII_TEMPLATE;
|
|
155
|
+
break;
|
|
156
|
+
case 'metadata':
|
|
157
|
+
name = 'MaysonMetadata';
|
|
158
|
+
code = METADATA_TEMPLATE;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (name && code) {
|
|
163
|
+
const filePath = path.join(outputDir, `${name}${ext}`);
|
|
164
|
+
fs.writeFileSync(filePath, code, 'utf-8');
|
|
165
|
+
|
|
166
|
+
results.push({
|
|
167
|
+
name,
|
|
168
|
+
filePath,
|
|
169
|
+
importStatement: `import { ${name} } from './${name}';`,
|
|
170
|
+
jsxTag: `<${name} />`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,42 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { findRootLayout } from './find-root-layout.js';
|
|
2
|
-
import {
|
|
3
|
+
import { generateComponents, ComponentType, GeneratedComponentInfo } from './generators.js';
|
|
4
|
+
import { injectComponentsIntoLayout, InjectorOptions, InjectorResult } from './injector.js';
|
|
3
5
|
|
|
4
|
-
export { findRootLayout,
|
|
5
|
-
export type { InjectorOptions, InjectorResult };
|
|
6
|
+
export { findRootLayout, generateComponents, injectComponentsIntoLayout };
|
|
7
|
+
export type { ComponentType, GeneratedComponentInfo, InjectorOptions, InjectorResult };
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* Convenience function to
|
|
10
|
+
* Convenience function to auto-detect Next.js App Router root layout, generate component files, and inject them.
|
|
9
11
|
*/
|
|
10
|
-
export function runAutoInject(
|
|
12
|
+
export function runAutoInject(
|
|
13
|
+
projectRoot: string = process.cwd(),
|
|
14
|
+
types: ComponentType[] = ['badge'],
|
|
15
|
+
dryRun: boolean = false
|
|
16
|
+
): InjectorResult {
|
|
11
17
|
const layoutPath = findRootLayout(projectRoot);
|
|
12
18
|
|
|
13
19
|
if (!layoutPath) {
|
|
14
20
|
return {
|
|
15
21
|
success: false,
|
|
16
22
|
filePath: '',
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
injectedComponents: [],
|
|
24
|
+
skippedComponents: [],
|
|
25
|
+
error: `Could not locate Next.js App Router root layout in ${projectRoot}`,
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
const isTypeScript = layoutPath.endsWith('.tsx') || layoutPath.endsWith('.ts');
|
|
30
|
+
const targetDir = path.dirname(layoutPath);
|
|
31
|
+
|
|
32
|
+
const generatedComponents = generateComponents(types, {
|
|
33
|
+
outputDir: targetDir,
|
|
34
|
+
isTypeScript,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return injectComponentsIntoLayout({
|
|
23
38
|
filePath: layoutPath,
|
|
24
|
-
|
|
39
|
+
components: generatedComponents,
|
|
25
40
|
dryRun,
|
|
26
41
|
});
|
|
27
42
|
}
|
package/src/injector.ts
CHANGED
|
@@ -1,101 +1,108 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { GeneratedComponentInfo } from './generators.js';
|
|
2
4
|
|
|
3
5
|
export interface InjectorOptions {
|
|
4
6
|
filePath: string;
|
|
5
|
-
|
|
7
|
+
components: GeneratedComponentInfo[];
|
|
6
8
|
dryRun?: boolean;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface InjectorResult {
|
|
10
12
|
success: boolean;
|
|
11
13
|
filePath: string;
|
|
12
|
-
|
|
14
|
+
injectedComponents: string[];
|
|
15
|
+
skippedComponents: string[];
|
|
13
16
|
modifiedContent?: string;
|
|
14
17
|
error?: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export function injectMaysonIframe(options: InjectorOptions): InjectorResult {
|
|
20
|
-
const { filePath, iframeUrl = 'https://mayson.dev/', dryRun = false } = options;
|
|
20
|
+
export function injectComponentsIntoLayout(options: InjectorOptions): InjectorResult {
|
|
21
|
+
const { filePath, components, dryRun = false } = options;
|
|
21
22
|
|
|
22
23
|
if (!fs.existsSync(filePath)) {
|
|
23
24
|
return {
|
|
24
25
|
success: false,
|
|
25
26
|
filePath,
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
injectedComponents: [],
|
|
28
|
+
skippedComponents: [],
|
|
29
|
+
error: `Layout file not found: ${filePath}`,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
const injectedComponents: string[] = [];
|
|
35
|
+
const skippedComponents: string[] = [];
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
success: true,
|
|
37
|
-
filePath,
|
|
38
|
-
alreadyInjected: true,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
37
|
+
for (const comp of components) {
|
|
38
|
+
const importMarker = comp.name;
|
|
39
|
+
const jsxMarker = `<${comp.name}`;
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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);
|
|
41
|
+
// Skip if already injected
|
|
42
|
+
if (content.includes(importMarker) || content.includes(jsxMarker)) {
|
|
43
|
+
skippedComponents.push(comp.name);
|
|
44
|
+
continue;
|
|
79
45
|
}
|
|
80
|
-
}
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
47
|
+
// Determine component file import path relative to layout.tsx
|
|
48
|
+
const layoutDir = path.dirname(filePath);
|
|
49
|
+
const compDir = path.dirname(comp.filePath);
|
|
50
|
+
const compBaseName = path.basename(comp.filePath, path.extname(comp.filePath));
|
|
51
|
+
|
|
52
|
+
let relativeImportPath = path.relative(layoutDir, path.join(compDir, compBaseName));
|
|
53
|
+
if (!relativeImportPath.startsWith('.')) {
|
|
54
|
+
relativeImportPath = `./${relativeImportPath}`;
|
|
55
|
+
}
|
|
56
|
+
// Convert backslashes for Windows path consistency
|
|
57
|
+
relativeImportPath = relativeImportPath.replace(/\\/g, '/');
|
|
58
|
+
|
|
59
|
+
const importLine = `import { ${comp.name} } from '${relativeImportPath}';\n`;
|
|
60
|
+
|
|
61
|
+
// 1. Insert import statement
|
|
62
|
+
if (/import\s+.*?from\s+['"].*?['"];?/g.test(content)) {
|
|
63
|
+
// Insert after the last import statement
|
|
64
|
+
const importMatches = Array.from(content.matchAll(/import\s+.*?from\s+['"].*?['"];?/g));
|
|
65
|
+
const lastMatch = importMatches[importMatches.length - 1];
|
|
66
|
+
if (lastMatch && lastMatch.index !== undefined) {
|
|
67
|
+
const insertPos = lastMatch.index + lastMatch[0].length;
|
|
68
|
+
content = content.slice(0, insertPos) + '\n' + importLine + content.slice(insertPos);
|
|
69
|
+
} else {
|
|
70
|
+
content = importLine + content;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
content = importLine + content;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Insert JSX tag into return block
|
|
77
|
+
const jsxSnippet = `\n {/* @mayson-component-injected: ${comp.name} */}\n ${comp.jsxTag}`;
|
|
78
|
+
|
|
79
|
+
if (/<\/body>/i.test(content)) {
|
|
80
|
+
content = content.replace(/<\/body>/i, `${jsxSnippet}\n </body>`);
|
|
81
|
+
} else if (/<\/html>/i.test(content)) {
|
|
82
|
+
content = content.replace(/<\/html>/i, `${jsxSnippet}\n </html>`);
|
|
83
|
+
} else {
|
|
84
|
+
const lastClosingIndex = content.lastIndexOf('</');
|
|
85
|
+
if (lastClosingIndex !== -1) {
|
|
86
|
+
content =
|
|
87
|
+
content.slice(0, lastClosingIndex) +
|
|
88
|
+
jsxSnippet +
|
|
89
|
+
'\n ' +
|
|
90
|
+
content.slice(lastClosingIndex);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
injectedComponents.push(comp.name);
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
if (!dryRun) {
|
|
92
|
-
fs.writeFileSync(filePath,
|
|
97
|
+
if (injectedComponents.length > 0 && !dryRun) {
|
|
98
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
return {
|
|
96
102
|
success: true,
|
|
97
103
|
filePath,
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
injectedComponents,
|
|
105
|
+
skippedComponents,
|
|
106
|
+
modifiedContent: content,
|
|
100
107
|
};
|
|
101
108
|
}
|