@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 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 (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/`.
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 custom iframe URL
19
- npx @mayson/inject-script --url=https://mayson.dev/my-app
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, injectMaysonIframe } from 'inject-script';
33
+ import { runAutoInject, findRootLayout, generateComponents, injectComponentsIntoLayout } from '@mayson-org/inject-script';
52
34
 
53
- // 1. Auto-detect and inject in one call
54
- const result = runAutoInject(process.cwd(), 'https://mayson.dev/');
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(`Injected to: ${result.filePath}`);
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
- url: 'https://mayson.dev/',
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('--url=')) {
69
- options.url = arg.split('=')[1];
70
- } else if (arg === '--url' && args[i + 1]) {
71
- options.url = args[++i];
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
- Inject Script CLI - Next.js App Router Root Layout Iframe Injector
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
- --url <url> Iframe URL to inject (default: https://mayson.dev/)
93
- --dry-run Simulate layout detection and code injection without modifying files
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 content = fs.readFileSync(layoutFile, 'utf-8');
111
- const INJECTION_MARKER = '/* @mayson-iframe-injected */';
214
+ const layoutDir = path.dirname(layoutFile);
215
+ const isTypeScript = layoutFile.endsWith('.tsx') || layoutFile.endsWith('.ts');
216
+ const ext = isTypeScript ? '.tsx' : '.jsx';
112
217
 
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
- }
218
+ const selectedTypes = options.type === 'all' ? ['badge', 'watermark', 'ascii', 'metadata'] : [options.type];
117
219
 
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);
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
- if (!updatedContent) {
150
- console.error(`❌ Error: Could not find suitable insertion point in ${layoutFile}`);
151
- process.exit(1);
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 (options.dryRun) {
155
- console.log(`🧪 [Dry Run] Would inject iframe to ${layoutFile}`);
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
- fs.writeFileSync(layoutFile, updatedContent, 'utf-8');
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,5 +1,2 @@
1
- /**
2
- * Finds the most likely Next.js App Router root layout file in a project.
3
- */
4
1
  export declare function findRootLayout(projectRoot?: string): string | null;
5
2
  //# sourceMappingURL=find-root-layout.d.ts.map
@@ -1 +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"}
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"}
@@ -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; // Shorter path depth is preferred
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 { injectMaysonIframe, InjectorOptions, InjectorResult } from './injector.js';
3
- export { findRootLayout, injectMaysonIframe };
4
- export type { InjectorOptions, InjectorResult };
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 automatically locate and inject the Mayson iframe into a Next.js project.
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, iframeUrl?: string, dryRun?: boolean): InjectorResult;
9
+ export declare function runAutoInject(projectRoot?: string, types?: ComponentType[], dryRun?: boolean): InjectorResult;
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
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 { injectMaysonIframe } from './injector.js';
3
- export { findRootLayout, injectMaysonIframe };
3
+ import { generateComponents } from './generators.js';
4
+ import { injectComponentsIntoLayout } from './injector.js';
5
+ export { findRootLayout, generateComponents, injectComponentsIntoLayout };
4
6
  /**
5
- * Convenience function to automatically locate and inject the Mayson iframe into a Next.js project.
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(), iframeUrl = 'https://mayson.dev/', dryRun = false) {
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
- alreadyInjected: false,
14
- error: `Could not locate Next.js App Router root layout file in ${projectRoot}`,
15
+ injectedComponents: [],
16
+ skippedComponents: [],
17
+ error: `Could not locate Next.js App Router root layout in ${projectRoot}`,
15
18
  };
16
19
  }
17
- return injectMaysonIframe({
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
- iframeUrl,
28
+ components: generatedComponents,
20
29
  dryRun,
21
30
  });
22
31
  }
@@ -1,14 +1,16 @@
1
+ import { GeneratedComponentInfo } from './generators.js';
1
2
  export interface InjectorOptions {
2
3
  filePath: string;
3
- iframeUrl?: string;
4
+ components: GeneratedComponentInfo[];
4
5
  dryRun?: boolean;
5
6
  }
6
7
  export interface InjectorResult {
7
8
  success: boolean;
8
9
  filePath: string;
9
- alreadyInjected: boolean;
10
+ injectedComponents: string[];
11
+ skippedComponents: string[];
10
12
  modifiedContent?: string;
11
13
  error?: string;
12
14
  }
13
- export declare function injectMaysonIframe(options: InjectorOptions): InjectorResult;
15
+ export declare function injectComponentsIntoLayout(options: InjectorOptions): InjectorResult;
14
16
  //# sourceMappingURL=injector.d.ts.map
@@ -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,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"}
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
- const INJECTION_MARKER = '/* @mayson-iframe-injected */';
3
- export function injectMaysonIframe(options) {
4
- const { filePath, iframeUrl = 'https://mayson.dev/', dryRun = false } = options;
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
- alreadyInjected: false,
10
- error: `File not found: ${filePath}`,
9
+ injectedComponents: [],
10
+ skippedComponents: [],
11
+ error: `Layout file not found: ${filePath}`,
11
12
  };
12
13
  }
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);
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 (!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');
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
- alreadyInjected: false,
74
- modifiedContent: updatedContent,
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.0",
4
- "description": "CLI tool and utility to dynamically locate Next.js App Router root layout files and inject the Mayson iframe script/component.",
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
- "iframe",
38
+ "badge",
39
+ "watermark",
40
+ "metadata",
39
41
  "mayson",
40
42
  "cli"
41
43
  ],
42
44
  "author": "",
43
45
  "license": "MIT"
44
- }
46
+ }
@@ -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; // Shorter path depth is preferred
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
- // 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);
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 { injectMaysonIframe, InjectorOptions, InjectorResult } from './injector.js';
3
+ import { generateComponents, ComponentType, GeneratedComponentInfo } from './generators.js';
4
+ import { injectComponentsIntoLayout, InjectorOptions, InjectorResult } from './injector.js';
3
5
 
4
- export { findRootLayout, injectMaysonIframe };
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 automatically locate and inject the Mayson iframe into a Next.js project.
10
+ * Convenience function to auto-detect Next.js App Router root layout, generate component files, and inject them.
9
11
  */
10
- export function runAutoInject(projectRoot: string = process.cwd(), iframeUrl: string = 'https://mayson.dev/', dryRun: boolean = false): InjectorResult {
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
- alreadyInjected: false,
18
- error: `Could not locate Next.js App Router root layout file in ${projectRoot}`,
23
+ injectedComponents: [],
24
+ skippedComponents: [],
25
+ error: `Could not locate Next.js App Router root layout in ${projectRoot}`,
19
26
  };
20
27
  }
21
28
 
22
- return injectMaysonIframe({
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
- iframeUrl,
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
- iframeUrl?: string;
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
- alreadyInjected: boolean;
14
+ injectedComponents: string[];
15
+ skippedComponents: string[];
13
16
  modifiedContent?: string;
14
17
  error?: string;
15
18
  }
16
19
 
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;
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
- alreadyInjected: false,
27
- error: `File not found: ${filePath}`,
27
+ injectedComponents: [],
28
+ skippedComponents: [],
29
+ error: `Layout file not found: ${filePath}`,
28
30
  };
29
31
  }
30
32
 
31
- const content = fs.readFileSync(filePath, 'utf-8');
33
+ let content = fs.readFileSync(filePath, 'utf-8');
34
+ const injectedComponents: string[] = [];
35
+ const skippedComponents: string[] = [];
32
36
 
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
- }
37
+ for (const comp of components) {
38
+ const importMarker = comp.name;
39
+ const jsxMarker = `<${comp.name}`;
41
40
 
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);
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
- 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
- };
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, updatedContent, 'utf-8');
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
- alreadyInjected: false,
99
- modifiedContent: updatedContent,
104
+ injectedComponents,
105
+ skippedComponents,
106
+ modifiedContent: content,
100
107
  };
101
108
  }