@mayson-org/inject-script 1.0.0 → 1.0.3
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/.env.example +9 -0
- package/README.md +57 -45
- package/bin/cli.mjs +1 -158
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/core/find-root-layout.d.ts +2 -0
- package/dist/core/find-root-layout.d.ts.map +1 -0
- package/dist/core/find-root-layout.js +61 -0
- package/dist/core/generators.d.ts +4 -0
- package/dist/core/generators.d.ts.map +1 -0
- package/dist/core/generators.js +37 -0
- package/dist/core/injector.d.ts +4 -0
- package/dist/core/injector.d.ts.map +1 -0
- package/dist/core/injector.js +76 -0
- package/dist/core/types.d.ts +48 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- 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 +134 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -9
- package/dist/injector.d.ts +5 -3
- package/dist/injector.d.ts.map +1 -1
- package/dist/injector.js +67 -61
- package/dist/plugins/ascii/index.d.ts +3 -0
- package/dist/plugins/ascii/index.d.ts.map +1 -0
- package/dist/plugins/ascii/index.js +25 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +9 -0
- package/dist/plugins/metadata/index.d.ts +3 -0
- package/dist/plugins/metadata/index.d.ts.map +1 -0
- package/dist/plugins/metadata/index.js +14 -0
- package/dist/plugins/watermark/index.d.ts +4 -0
- package/dist/plugins/watermark/index.d.ts.map +1 -0
- package/dist/plugins/watermark/index.js +7 -0
- package/dist/plugins/watermark/template.d.ts +9 -0
- package/dist/plugins/watermark/template.d.ts.map +1 -0
- package/dist/plugins/watermark/template.js +128 -0
- package/dist/shared/config.d.ts +13 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +25 -0
- package/dist/shared/generated-app-metadata.d.ts +28 -0
- package/dist/shared/generated-app-metadata.d.ts.map +1 -0
- package/dist/shared/generated-app-metadata.js +103 -0
- package/dist/shared/remix-url.d.ts +13 -0
- package/dist/shared/remix-url.d.ts.map +1 -0
- package/dist/shared/remix-url.js +16 -0
- package/package.json +15 -8
- package/src/find-root-layout.ts +0 -88
- package/src/index.ts +0 -27
- package/src/injector.ts +0 -101
package/.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Set before running inject-script (e.g. in CI / Vercel build env)
|
|
2
|
+
|
|
3
|
+
MAYSON_WORKSPACE_ID=
|
|
4
|
+
MAYSON_COLLECTION_ID=
|
|
5
|
+
MAYSON_API_BASE_URL=
|
|
6
|
+
MAYSON_APP_BASE_URL=https://mayson.dev
|
|
7
|
+
|
|
8
|
+
# Auth for generated-app-metadata (show_remix_pill / enable_remix)
|
|
9
|
+
MAYSON_WEB_TOKEN=
|
package/README.md
CHANGED
|
@@ -1,71 +1,83 @@
|
|
|
1
|
-
# @mayson/inject-script
|
|
1
|
+
# @mayson-org/inject-script
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Locate the Next.js App Router root layout, generate Mayson plugin components next to it, and inject imports + JSX into the layout. Injection logic is unchanged; plugins supply component source.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Requires **Node 20+**.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
Run directly in any Next.js project root:
|
|
7
|
+
## Install
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
|
-
|
|
10
|
+
bun add -d @mayson-org/inject-script@latest
|
|
11
|
+
# or
|
|
12
|
+
npx @mayson-org/inject-script --type=watermark
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Quick usage
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
19
|
-
npx @mayson/inject-script --
|
|
18
|
+
npx @mayson-org/inject-script --type=watermark
|
|
19
|
+
npx @mayson-org/inject-script --type=all
|
|
20
|
+
npx @mayson-org/inject-script --type=watermark --dry-run
|
|
21
|
+
npx @mayson-org/inject-script --dir=./my-app --type=watermark
|
|
22
|
+
```
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
npx @mayson/inject-script --dry-run
|
|
24
|
+
## Watermark
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
npx @mayson/inject-script --dir=./my-nextjs-app
|
|
26
|
-
```
|
|
26
|
+
`--type=watermark` writes `MaysonWatermark.tsx` beside the root layout and injects `<MaysonWatermark />` before `</body>`.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
The pill:
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
- Fixed bottom-right “Edit with Mayson” (logo + label)
|
|
31
|
+
- Dismiss (×) hides it for the session
|
|
32
|
+
- Click opens the Mayson remix URL (baked at inject time)
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
### API gating (`show_remix_pill` / `enable_remix`)
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
"scripts": {
|
|
37
|
-
"dev": "next dev",
|
|
38
|
-
"prebuild": "npx @mayson/inject-script",
|
|
39
|
-
"build": "next build"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
```
|
|
36
|
+
At inject time the CLI fetches **generated-app-metadata** (same API as the old build plugins):
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
`GET {MAYSON_API_BASE_URL}/sigma/web/v1/generated-app-metadata/workspaces/{id}/collections/{id}`
|
|
39
|
+
Header: `M-Web-Token: {MAYSON_WEB_TOKEN}`
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
| API field | Effect |
|
|
42
|
+
|-----------|--------|
|
|
43
|
+
| `show_remix_pill` | If `false`, watermark is **not** injected; if `true`, inject |
|
|
44
|
+
| `enable_remix` | Baked into the component remix URL (`true` → collection remix redirect; otherwise app home) |
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
**Metadata not found (404):** if the API returns HTTP 404 or a body like `{ "response_code": 404, "message": "Generated app metadata not found", "value": {} }`, treat as **`show_remix_pill=true`** and **`enable_remix=true`** (show watermark, remixable).
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
import { runAutoInject, findRootLayout, injectMaysonIframe } from 'inject-script';
|
|
48
|
+
Defaults when the API is not called or fails for other reasons: `showRemixPill=false`, `enableRemix=false` (watermark skipped).
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
const result = runAutoInject(process.cwd(), 'https://mayson.dev/');
|
|
50
|
+
### Env vars (inject-time)
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
| Variable | Purpose |
|
|
53
|
+
|----------|---------|
|
|
54
|
+
| `MAYSON_WORKSPACE_ID` | Workspace id |
|
|
55
|
+
| `MAYSON_COLLECTION_ID` | Collection id (also baked into remix URL) |
|
|
56
|
+
| `MAYSON_API_BASE_URL` | API base for metadata |
|
|
57
|
+
| `MAYSON_APP_BASE_URL` | App base (default `https://mayson.dev`) |
|
|
58
|
+
| `MAYSON_WEB_TOKEN` | Auth for generated-app-metadata |
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
## Programmatic API
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { runAutoInject } from '@mayson-org/inject-script';
|
|
64
|
+
|
|
65
|
+
const result = await runAutoInject(process.cwd(), ['watermark']);
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
## Package layout
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
src/
|
|
72
|
+
cli.ts / index.ts
|
|
73
|
+
core/ # find-root-layout, injector, generators
|
|
74
|
+
plugins/watermark/ # remix pill template
|
|
75
|
+
plugins/ascii|metadata # stubs for later restores
|
|
76
|
+
shared/ # config, generated-app-metadata, remix-url
|
|
77
|
+
```
|
|
66
78
|
|
|
67
|
-
##
|
|
79
|
+
## Notes
|
|
68
80
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
81
|
+
- Default `--type` is `watermark`.
|
|
82
|
+
- Idempotent: skips components already present in the layout.
|
|
83
|
+
- Does not patch `.next` HTML; source inject only.
|
package/bin/cli.mjs
CHANGED
|
@@ -1,159 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
function findLayoutFiles(dir, fileList = []) {
|
|
7
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
8
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
9
|
-
|
|
10
|
-
for (const entry of entries) {
|
|
11
|
-
const fullPath = path.join(dir, entry.name);
|
|
12
|
-
if (entry.isDirectory()) {
|
|
13
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
14
|
-
findLayoutFiles(fullPath, fileList);
|
|
15
|
-
} else if (entry.isFile()) {
|
|
16
|
-
const ext = path.extname(entry.name);
|
|
17
|
-
const base = path.basename(entry.name, ext);
|
|
18
|
-
if (base === 'layout' && ['.tsx', '.jsx', '.ts', '.js'].includes(ext)) {
|
|
19
|
-
fileList.push(fullPath);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return fileList;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function scoreLayout(filePath, projectRoot) {
|
|
27
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
29
|
-
const depth = relativePath.split(path.sep).length;
|
|
30
|
-
let score = 100 - depth;
|
|
31
|
-
|
|
32
|
-
if (/<html/i.test(content)) score += 50;
|
|
33
|
-
if (/<body/i.test(content)) score += 50;
|
|
34
|
-
if (/^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) || /^app[/\\]layout\.[jt]sx?$/.test(relativePath)) {
|
|
35
|
-
score += 40;
|
|
36
|
-
}
|
|
37
|
-
return score;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function findRootLayout(projectRoot) {
|
|
41
|
-
const possibleDirs = [path.join(projectRoot, 'src', 'app'), path.join(projectRoot, 'app')];
|
|
42
|
-
const candidates = [];
|
|
43
|
-
|
|
44
|
-
for (const dir of possibleDirs) {
|
|
45
|
-
if (fs.existsSync(dir)) findLayoutFiles(dir, candidates);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (candidates.length === 0) return null;
|
|
49
|
-
candidates.sort((a, b) => scoreLayout(b, projectRoot) - scoreLayout(a, projectRoot));
|
|
50
|
-
return candidates[0];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function parseArgs() {
|
|
54
|
-
const args = process.argv.slice(2);
|
|
55
|
-
const options = {
|
|
56
|
-
projectRoot: process.cwd(),
|
|
57
|
-
url: 'https://mayson.dev/',
|
|
58
|
-
dryRun: false,
|
|
59
|
-
help: false,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < args.length; i++) {
|
|
63
|
-
const arg = args[i];
|
|
64
|
-
if (arg === '--help' || arg === '-h') {
|
|
65
|
-
options.help = true;
|
|
66
|
-
} else if (arg === '--dry-run') {
|
|
67
|
-
options.dryRun = true;
|
|
68
|
-
} else if (arg.startsWith('--url=')) {
|
|
69
|
-
options.url = arg.split('=')[1];
|
|
70
|
-
} else if (arg === '--url' && args[i + 1]) {
|
|
71
|
-
options.url = args[++i];
|
|
72
|
-
} else if (arg.startsWith('--dir=')) {
|
|
73
|
-
options.projectRoot = path.resolve(arg.split('=')[1]);
|
|
74
|
-
} else if (arg === '--dir' && args[i + 1]) {
|
|
75
|
-
options.projectRoot = path.resolve(args[++i]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return options;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const options = parseArgs();
|
|
82
|
-
|
|
83
|
-
if (options.help) {
|
|
84
|
-
console.log(`
|
|
85
|
-
Inject Script CLI - Next.js App Router Root Layout Iframe Injector
|
|
86
|
-
|
|
87
|
-
Usage:
|
|
88
|
-
npx inject-script [options]
|
|
89
|
-
|
|
90
|
-
Options:
|
|
91
|
-
--dir <path> Target project root directory (default: current working directory)
|
|
92
|
-
--url <url> Iframe URL to inject (default: https://mayson.dev/)
|
|
93
|
-
--dry-run Simulate layout detection and code injection without modifying files
|
|
94
|
-
--help, -h Show this help message
|
|
95
|
-
`);
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(`🔍 Searching for Next.js root layout in: ${options.projectRoot}`);
|
|
100
|
-
|
|
101
|
-
const layoutFile = findRootLayout(options.projectRoot);
|
|
102
|
-
|
|
103
|
-
if (!layoutFile) {
|
|
104
|
-
console.error(`❌ Error: Could not find any App Router layout file in ${options.projectRoot}`);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.log(`📍 Found Root Layout: ${path.relative(options.projectRoot, layoutFile)}`);
|
|
109
|
-
|
|
110
|
-
const content = fs.readFileSync(layoutFile, 'utf-8');
|
|
111
|
-
const INJECTION_MARKER = '/* @mayson-iframe-injected */';
|
|
112
|
-
|
|
113
|
-
if (content.includes(INJECTION_MARKER) || content.includes(options.url)) {
|
|
114
|
-
console.log(`✅ Mayson iframe is already injected in ${path.basename(layoutFile)}.`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const snippet = `
|
|
119
|
-
{${INJECTION_MARKER}}
|
|
120
|
-
<iframe
|
|
121
|
-
src="${options.url}"
|
|
122
|
-
style={{
|
|
123
|
-
position: 'fixed',
|
|
124
|
-
bottom: '20px',
|
|
125
|
-
right: '20px',
|
|
126
|
-
width: '400px',
|
|
127
|
-
height: '600px',
|
|
128
|
-
border: 'none',
|
|
129
|
-
borderRadius: '12px',
|
|
130
|
-
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.25)',
|
|
131
|
-
zIndex: 999999,
|
|
132
|
-
}}
|
|
133
|
-
title="Mayson Dev Overlay"
|
|
134
|
-
/>`;
|
|
135
|
-
|
|
136
|
-
let updatedContent = null;
|
|
137
|
-
|
|
138
|
-
if (/<\/body>/i.test(content)) {
|
|
139
|
-
updatedContent = content.replace(/<\/body>/i, `${snippet}\n </body>`);
|
|
140
|
-
} else if (/<\/html>/i.test(content)) {
|
|
141
|
-
updatedContent = content.replace(/<\/html>/i, `${snippet}\n </html>`);
|
|
142
|
-
} else {
|
|
143
|
-
const lastIndex = content.lastIndexOf('</');
|
|
144
|
-
if (lastIndex !== -1) {
|
|
145
|
-
updatedContent = content.slice(0, lastIndex) + snippet + '\n ' + content.slice(lastIndex);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!updatedContent) {
|
|
150
|
-
console.error(`❌ Error: Could not find suitable insertion point in ${layoutFile}`);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (options.dryRun) {
|
|
155
|
-
console.log(`🧪 [Dry Run] Would inject iframe to ${layoutFile}`);
|
|
156
|
-
} else {
|
|
157
|
-
fs.writeFileSync(layoutFile, updatedContent, 'utf-8');
|
|
158
|
-
console.log(`🎉 Successfully injected Mayson iframe (${options.url}) into ${path.relative(options.projectRoot, layoutFile)}!`);
|
|
159
|
-
}
|
|
2
|
+
import '../dist/cli.js';
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAwDA,wBAAsB,IAAI,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DtE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runAutoInject } from './index.js';
|
|
4
|
+
function printHelp() {
|
|
5
|
+
console.log(`
|
|
6
|
+
@mayson-org/inject-script — generate & inject Mayson components into Next.js App Router layout
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
inject-script [options]
|
|
10
|
+
npx @mayson-org/inject-script [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--dir <path> Target project root (default: cwd)
|
|
14
|
+
--type <type> watermark | ascii | metadata | all (default: watermark)
|
|
15
|
+
--dry-run Preview without writing files
|
|
16
|
+
--help, -h Show help
|
|
17
|
+
|
|
18
|
+
Watermark gating (inject-time):
|
|
19
|
+
Fetches generated-app-metadata when MAYSON_WORKSPACE_ID, MAYSON_COLLECTION_ID,
|
|
20
|
+
MAYSON_API_BASE_URL, and MAYSON_WEB_TOKEN are set. Injects watermark only when
|
|
21
|
+
show_remix_pill is true; bakes enable_remix into the generated component.
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const options = {
|
|
26
|
+
projectRoot: process.cwd(),
|
|
27
|
+
type: 'watermark',
|
|
28
|
+
dryRun: false,
|
|
29
|
+
help: false,
|
|
30
|
+
};
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const arg = argv[i];
|
|
33
|
+
if (arg === '--help' || arg === '-h') {
|
|
34
|
+
options.help = true;
|
|
35
|
+
}
|
|
36
|
+
else if (arg === '--dry-run') {
|
|
37
|
+
options.dryRun = true;
|
|
38
|
+
}
|
|
39
|
+
else if (arg.startsWith('--type=')) {
|
|
40
|
+
options.type = arg.split('=')[1];
|
|
41
|
+
}
|
|
42
|
+
else if (arg === '--type' && argv[i + 1]) {
|
|
43
|
+
options.type = argv[++i];
|
|
44
|
+
}
|
|
45
|
+
else if (arg.startsWith('--dir=')) {
|
|
46
|
+
options.projectRoot = path.resolve(arg.split('=')[1]);
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--dir' && argv[i + 1]) {
|
|
49
|
+
options.projectRoot = path.resolve(argv[++i]);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
58
|
+
try {
|
|
59
|
+
const options = parseArgs(argv);
|
|
60
|
+
if (options.help) {
|
|
61
|
+
printHelp();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(`Searching for Next.js root layout in: ${options.projectRoot}`);
|
|
65
|
+
const result = await runAutoInject(options.projectRoot, [options.type], options.dryRun);
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
console.error(`Error: ${result.error}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const relativeLayout = path.relative(options.projectRoot, result.filePath);
|
|
72
|
+
console.log(`Found root layout: ${relativeLayout}`);
|
|
73
|
+
for (const message of result.messages ?? []) {
|
|
74
|
+
console.log(`[mayson] ${message}`);
|
|
75
|
+
}
|
|
76
|
+
if (options.dryRun) {
|
|
77
|
+
console.log(`[dry-run] Would inject: ${result.injectedComponents.join(', ') || '(none)'}`);
|
|
78
|
+
if (result.skippedComponents.length > 0) {
|
|
79
|
+
console.log(`[dry-run] Already present: ${result.skippedComponents.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (result.injectedComponents.length > 0) {
|
|
84
|
+
console.log(`Injected: ${result.injectedComponents.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
if (result.skippedComponents.length > 0) {
|
|
87
|
+
console.log(`Skipped (already present): ${result.skippedComponents.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (result.injectedComponents.length === 0 &&
|
|
90
|
+
result.skippedComponents.length === 0 &&
|
|
91
|
+
(result.messages?.length ?? 0) > 0) {
|
|
92
|
+
console.log('Nothing to inject.');
|
|
93
|
+
}
|
|
94
|
+
else if (result.injectedComponents.length === 0 &&
|
|
95
|
+
result.skippedComponents.length > 0) {
|
|
96
|
+
console.log('Root layout is up to date.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error(`Error: ${err.message}`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
void main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-root-layout.d.ts","sourceRoot":"","sources":["../../src/core/find-root-layout.ts"],"names":[],"mappings":"AAgDA,wBAAgB,cAAc,CAAC,WAAW,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CA0BjF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
4
|
+
function findLayoutFiles(dir, fileList = []) {
|
|
5
|
+
if (!fs.existsSync(dir))
|
|
6
|
+
return fileList;
|
|
7
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const fullPath = path.join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
findLayoutFiles(fullPath, fileList);
|
|
15
|
+
}
|
|
16
|
+
else if (entry.isFile()) {
|
|
17
|
+
const ext = path.extname(entry.name);
|
|
18
|
+
const base = path.basename(entry.name, ext);
|
|
19
|
+
if (base === 'layout' && LAYOUT_EXTENSIONS.includes(ext)) {
|
|
20
|
+
fileList.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return fileList;
|
|
25
|
+
}
|
|
26
|
+
function scoreLayoutCandidate(filePath, projectRoot) {
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
29
|
+
const depth = relativePath.split(path.sep).length;
|
|
30
|
+
let score = 100 - depth;
|
|
31
|
+
if (/<html/i.test(content))
|
|
32
|
+
score += 50;
|
|
33
|
+
if (/<body/i.test(content))
|
|
34
|
+
score += 50;
|
|
35
|
+
const isDirectAppChild = /^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
|
|
36
|
+
/^app[/\\]layout\.[jt]sx?$/.test(relativePath);
|
|
37
|
+
if (isDirectAppChild)
|
|
38
|
+
score += 40;
|
|
39
|
+
return score;
|
|
40
|
+
}
|
|
41
|
+
export function findRootLayout(projectRoot = process.cwd()) {
|
|
42
|
+
const possibleAppDirs = [
|
|
43
|
+
path.join(projectRoot, 'src', 'app'),
|
|
44
|
+
path.join(projectRoot, 'app'),
|
|
45
|
+
];
|
|
46
|
+
const candidateFiles = [];
|
|
47
|
+
for (const appDir of possibleAppDirs) {
|
|
48
|
+
if (fs.existsSync(appDir)) {
|
|
49
|
+
findLayoutFiles(appDir, candidateFiles);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (candidateFiles.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const scored = candidateFiles.map((filePath) => ({
|
|
56
|
+
filePath,
|
|
57
|
+
score: scoreLayoutCandidate(filePath, projectRoot),
|
|
58
|
+
}));
|
|
59
|
+
scored.sort((a, b) => b.score - a.score);
|
|
60
|
+
return scored[0]?.filePath || null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ComponentGeneratorOptions, ComponentType, GeneratedComponentInfo } from './types.js';
|
|
2
|
+
export type { ComponentGeneratorOptions, ComponentType, GeneratedComponentInfo, } from './types.js';
|
|
3
|
+
export declare function generateComponents(types: ComponentType[] | undefined, options: ComponentGeneratorOptions): GeneratedComponentInfo[];
|
|
4
|
+
//# sourceMappingURL=generators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generators.d.ts","sourceRoot":"","sources":["../../src/core/generators.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACb,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,yBAAyB,EACzB,aAAa,EACb,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAQpB,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,aAAa,EAAE,YAAgB,EACtC,OAAO,EAAE,yBAAyB,GACjC,sBAAsB,EAAE,CAkC1B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { plugins } from '../plugins/index.js';
|
|
4
|
+
const ALL_TYPES = [
|
|
5
|
+
'watermark',
|
|
6
|
+
'ascii',
|
|
7
|
+
'metadata',
|
|
8
|
+
];
|
|
9
|
+
export function generateComponents(types = ['watermark'], options) {
|
|
10
|
+
const { outputDir, isTypeScript = true, dryRun = false } = options;
|
|
11
|
+
const ext = isTypeScript ? '.tsx' : '.jsx';
|
|
12
|
+
const results = [];
|
|
13
|
+
if (!dryRun && !fs.existsSync(outputDir)) {
|
|
14
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
const selectedTypes = types.includes('all')
|
|
17
|
+
? ALL_TYPES
|
|
18
|
+
: types.filter((t) => t !== 'all');
|
|
19
|
+
for (const type of selectedTypes) {
|
|
20
|
+
const plugin = plugins[type];
|
|
21
|
+
if (!plugin) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const code = plugin.generateSource(options.context);
|
|
25
|
+
const filePath = path.join(outputDir, `${plugin.name}${ext}`);
|
|
26
|
+
if (!dryRun) {
|
|
27
|
+
fs.writeFileSync(filePath, code, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
results.push({
|
|
30
|
+
name: plugin.name,
|
|
31
|
+
filePath,
|
|
32
|
+
importStatement: `import { ${plugin.name} } from './${plugin.name}';`,
|
|
33
|
+
jsxTag: `<${plugin.name} />`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injector.d.ts","sourceRoot":"","sources":["../../src/core/injector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CAqFnF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function injectComponentsIntoLayout(options) {
|
|
4
|
+
const { filePath, components, dryRun = false } = options;
|
|
5
|
+
if (!fs.existsSync(filePath)) {
|
|
6
|
+
return {
|
|
7
|
+
success: false,
|
|
8
|
+
filePath,
|
|
9
|
+
injectedComponents: [],
|
|
10
|
+
skippedComponents: [],
|
|
11
|
+
error: `Layout file not found: ${filePath}`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
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;
|
|
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
|
+
const importMatches = Array.from(content.matchAll(/import\s+.*?from\s+['"].*?['"];?/g));
|
|
38
|
+
if (importMatches.length > 0) {
|
|
39
|
+
const lastMatch = importMatches[importMatches.length - 1];
|
|
40
|
+
const insertPos = lastMatch.index + lastMatch[0].length;
|
|
41
|
+
content = content.slice(0, insertPos) + '\n' + importLine + content.slice(insertPos);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
content = importLine + content;
|
|
45
|
+
}
|
|
46
|
+
// 2. Insert JSX tag into return block
|
|
47
|
+
const jsxSnippet = `\n {/* @mayson-component-injected: ${comp.name} */}\n ${comp.jsxTag}`;
|
|
48
|
+
if (/<\/body>/i.test(content)) {
|
|
49
|
+
content = content.replace(/<\/body>/i, `${jsxSnippet}\n </body>`);
|
|
50
|
+
}
|
|
51
|
+
else if (/<\/html>/i.test(content)) {
|
|
52
|
+
content = content.replace(/<\/html>/i, `${jsxSnippet}\n </html>`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const lastClosingIndex = content.lastIndexOf('</');
|
|
56
|
+
if (lastClosingIndex !== -1) {
|
|
57
|
+
content =
|
|
58
|
+
content.slice(0, lastClosingIndex) +
|
|
59
|
+
jsxSnippet +
|
|
60
|
+
'\n ' +
|
|
61
|
+
content.slice(lastClosingIndex);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
injectedComponents.push(comp.name);
|
|
65
|
+
}
|
|
66
|
+
if (injectedComponents.length > 0 && !dryRun) {
|
|
67
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
filePath,
|
|
72
|
+
injectedComponents,
|
|
73
|
+
skippedComponents,
|
|
74
|
+
modifiedContent: content,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type ComponentType = 'watermark' | 'ascii' | 'metadata' | 'all';
|
|
2
|
+
export interface GeneratedComponentInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
importStatement: string;
|
|
6
|
+
jsxTag: string;
|
|
7
|
+
}
|
|
8
|
+
/** Context passed into plugin generateSource (e.g. watermark flags from API). */
|
|
9
|
+
export type PluginGenerateContext = {
|
|
10
|
+
watermark?: {
|
|
11
|
+
showRemixPill: boolean;
|
|
12
|
+
enableRemix: boolean;
|
|
13
|
+
appBaseUrl: string;
|
|
14
|
+
collectionId: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export interface ComponentGeneratorOptions {
|
|
18
|
+
outputDir: string;
|
|
19
|
+
isTypeScript?: boolean;
|
|
20
|
+
/** When true, return component info without writing files. */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Plugin template context (API / env resolved flags). */
|
|
23
|
+
context?: PluginGenerateContext;
|
|
24
|
+
}
|
|
25
|
+
export interface InjectorOptions {
|
|
26
|
+
filePath: string;
|
|
27
|
+
components: GeneratedComponentInfo[];
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface InjectorResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
filePath: string;
|
|
33
|
+
injectedComponents: string[];
|
|
34
|
+
skippedComponents: string[];
|
|
35
|
+
modifiedContent?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
/** Extra notes (e.g. watermark skipped because show_remix_pill is false). */
|
|
38
|
+
messages?: string[];
|
|
39
|
+
}
|
|
40
|
+
/** Descriptor for a generate-and-inject plugin. */
|
|
41
|
+
export interface MaysonPlugin {
|
|
42
|
+
type: Exclude<ComponentType, 'all'>;
|
|
43
|
+
/** Exported React component name, e.g. MaysonWatermark */
|
|
44
|
+
name: string;
|
|
45
|
+
/** Source for the component file written next to the root layout */
|
|
46
|
+
generateSource: (context?: PluginGenerateContext) => string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;AAEvE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iFAAiF;AACjF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE;QACV,aAAa,EAAE,OAAO,CAAC;QACvB,WAAW,EAAE,OAAO,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,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;IACf,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;IACpC,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,cAAc,EAAE,CAAC,OAAO,CAAC,EAAE,qBAAqB,KAAK,MAAM,CAAC;CAC7D"}
|