@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { buildRemixRedirectUrl } from '../../shared/remix-url.js';
|
|
2
|
+
const LOGO_URL = 'https://mayson.dev/logo/brand-logo-dark.png';
|
|
3
|
+
function resolveWatermarkOptions(context) {
|
|
4
|
+
const wm = context?.watermark;
|
|
5
|
+
return {
|
|
6
|
+
enableRemix: wm?.enableRemix ?? false,
|
|
7
|
+
appBaseUrl: wm?.appBaseUrl || 'https://mayson.dev',
|
|
8
|
+
collectionId: wm?.collectionId || '',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Source for the consumer-app MaysonWatermark client component. */
|
|
12
|
+
export function buildMaysonWatermarkSource(context) {
|
|
13
|
+
const options = resolveWatermarkOptions(context);
|
|
14
|
+
const remixUrl = buildRemixRedirectUrl({
|
|
15
|
+
appBaseUrl: options.appBaseUrl,
|
|
16
|
+
collectionId: options.collectionId,
|
|
17
|
+
enableRemix: options.enableRemix,
|
|
18
|
+
});
|
|
19
|
+
return `'use client';
|
|
20
|
+
|
|
21
|
+
import React, { useState } from 'react';
|
|
22
|
+
|
|
23
|
+
const LOGO_URL = ${JSON.stringify(LOGO_URL)};
|
|
24
|
+
const REMIX_URL = ${JSON.stringify(remixUrl)};
|
|
25
|
+
|
|
26
|
+
export function MaysonWatermark() {
|
|
27
|
+
const [dismissed, setDismissed] = useState(false);
|
|
28
|
+
|
|
29
|
+
if (dismissed) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handleRemix = (event: React.MouseEvent) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
window.location.href = REMIX_URL;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDismiss = (event: React.MouseEvent) => {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
event.stopPropagation();
|
|
42
|
+
setDismissed(true);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
role="group"
|
|
48
|
+
aria-label="Edit with Mayson"
|
|
49
|
+
style={{
|
|
50
|
+
position: 'fixed',
|
|
51
|
+
right: 16,
|
|
52
|
+
bottom: 16,
|
|
53
|
+
zIndex: 2147483647,
|
|
54
|
+
display: 'inline-flex',
|
|
55
|
+
alignItems: 'flex-end',
|
|
56
|
+
gap: 8,
|
|
57
|
+
padding: '4px 8px',
|
|
58
|
+
borderRadius: 6,
|
|
59
|
+
background: '#171718',
|
|
60
|
+
border: '0.6px solid #46454866',
|
|
61
|
+
boxShadow: '0px 8px 12px 6px #00000026',
|
|
62
|
+
fontFamily: '"Google Sans", system-ui, -apple-system, sans-serif',
|
|
63
|
+
textTransform: 'none',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={handleRemix}
|
|
69
|
+
style={{
|
|
70
|
+
display: 'inline-flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: 4,
|
|
73
|
+
color: '#9D9DA2',
|
|
74
|
+
textDecoration: 'none',
|
|
75
|
+
cursor: 'pointer',
|
|
76
|
+
outline: 'none',
|
|
77
|
+
border: 'none',
|
|
78
|
+
background: 'transparent',
|
|
79
|
+
padding: 0,
|
|
80
|
+
margin: 0,
|
|
81
|
+
fontFamily: 'inherit',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<span
|
|
85
|
+
style={{
|
|
86
|
+
color: '#9D9DA2',
|
|
87
|
+
fontSize: 12,
|
|
88
|
+
fontWeight: 500,
|
|
89
|
+
lineHeight: '14px',
|
|
90
|
+
whiteSpace: 'nowrap',
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
Edit with
|
|
94
|
+
</span>
|
|
95
|
+
<img
|
|
96
|
+
src={LOGO_URL}
|
|
97
|
+
alt="Mayson"
|
|
98
|
+
style={{ display: 'block', height: 16, width: 'auto' }}
|
|
99
|
+
/>
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
aria-label="Dismiss Edit with Mayson"
|
|
104
|
+
onClick={handleDismiss}
|
|
105
|
+
style={{
|
|
106
|
+
display: 'inline-flex',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
justifyContent: 'center',
|
|
109
|
+
width: 18,
|
|
110
|
+
height: 18,
|
|
111
|
+
padding: 0,
|
|
112
|
+
border: 'none',
|
|
113
|
+
background: 'transparent',
|
|
114
|
+
color: '#9D9DA2',
|
|
115
|
+
fontSize: 16,
|
|
116
|
+
lineHeight: 1,
|
|
117
|
+
cursor: 'pointer',
|
|
118
|
+
flexShrink: 0,
|
|
119
|
+
fontFamily: 'inherit',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
×
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type MaysonEnvConfig = {
|
|
2
|
+
workspaceId: string;
|
|
3
|
+
collectionId: string;
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
appBaseUrl: string;
|
|
6
|
+
webToken: string;
|
|
7
|
+
/** Local defaults before API merge */
|
|
8
|
+
showRemixPill: boolean;
|
|
9
|
+
enableRemix: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Load Mayson platform env (inject-time / CLI). */
|
|
12
|
+
export declare function loadMaysonEnvConfig(): MaysonEnvConfig;
|
|
13
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAMF,oDAAoD;AACpD,wBAAgB,mBAAmB,IAAI,eAAe,CA0BrD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function readEnv(key) {
|
|
2
|
+
return process.env[key]?.trim() ?? '';
|
|
3
|
+
}
|
|
4
|
+
/** Load Mayson platform env (inject-time / CLI). */
|
|
5
|
+
export function loadMaysonEnvConfig() {
|
|
6
|
+
const workspaceId = readEnv('MAYSON_WORKSPACE_ID');
|
|
7
|
+
const collectionId = readEnv('MAYSON_COLLECTION_ID') || readEnv('NEXT_PUBLIC_MAYSON_COLLECTION_ID');
|
|
8
|
+
const apiBaseUrl = readEnv('MAYSON_API_BASE_URL');
|
|
9
|
+
const appBaseUrl = readEnv('MAYSON_APP_BASE_URL') ||
|
|
10
|
+
readEnv('NEXT_PUBLIC_MAYSON_APP_BASE_URL') ||
|
|
11
|
+
'https://mayson.dev';
|
|
12
|
+
const webToken = readEnv('MAYSON_WEB_TOKEN');
|
|
13
|
+
if (!workspaceId && !collectionId && !apiBaseUrl) {
|
|
14
|
+
console.warn('[mayson] missing MAYSON_WORKSPACE_ID, MAYSON_COLLECTION_ID, or MAYSON_API_BASE_URL — metadata fetch may be skipped');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
workspaceId,
|
|
18
|
+
collectionId,
|
|
19
|
+
apiBaseUrl,
|
|
20
|
+
appBaseUrl,
|
|
21
|
+
webToken,
|
|
22
|
+
showRemixPill: false,
|
|
23
|
+
enableRemix: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MaysonEnvConfig } from './config.js';
|
|
2
|
+
export type GeneratedAppMetadataValue = {
|
|
3
|
+
show_remix_pill?: boolean | null;
|
|
4
|
+
enable_remix?: boolean | null;
|
|
5
|
+
};
|
|
6
|
+
export type GeneratedAppMetadataResponse = {
|
|
7
|
+
response_code?: number;
|
|
8
|
+
response_type?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
value?: GeneratedAppMetadataValue;
|
|
11
|
+
};
|
|
12
|
+
export type FetchGeneratedAppMetadataOptions = {
|
|
13
|
+
webToken?: string;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
};
|
|
17
|
+
/** When metadata record does not exist yet — show pill and allow remix. */
|
|
18
|
+
export declare const METADATA_NOT_FOUND_DEFAULTS: Pick<MaysonEnvConfig, 'showRemixPill' | 'enableRemix'>;
|
|
19
|
+
export declare function resolveGeneratedAppMetadataUrl(config: Pick<MaysonEnvConfig, 'apiBaseUrl' | 'workspaceId' | 'collectionId'>): string | null;
|
|
20
|
+
export declare function shouldFetchGeneratedAppMetadata(config: MaysonEnvConfig, webToken?: string): boolean;
|
|
21
|
+
export declare function mapGeneratedAppMetadataValue(value: GeneratedAppMetadataValue): Partial<Pick<MaysonEnvConfig, 'showRemixPill' | 'enableRemix'>>;
|
|
22
|
+
/** True when API indicates no metadata row exists yet (HTTP or body 404). */
|
|
23
|
+
export declare function isGeneratedAppMetadataNotFound(httpStatus: number, data: GeneratedAppMetadataResponse | null | undefined): boolean;
|
|
24
|
+
/** Fetch generated-app-metadata. Returns null on hard failure (network / non-404 errors). */
|
|
25
|
+
export declare function fetchGeneratedAppMetadata(config: MaysonEnvConfig, options?: FetchGeneratedAppMetadataOptions): Promise<Partial<Pick<MaysonEnvConfig, 'showRemixPill' | 'enableRemix'>> | null>;
|
|
26
|
+
/** Local env config merged with generated-app-metadata when credentials are set. */
|
|
27
|
+
export declare function resolveMaysonConfig(options?: FetchGeneratedAppMetadataOptions): Promise<MaysonEnvConfig>;
|
|
28
|
+
//# sourceMappingURL=generated-app-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated-app-metadata.d.ts","sourceRoot":"","sources":["../../src/shared/generated-app-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,MAAM,MAAM,yBAAyB,GAAG;IACtC,eAAe,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,yBAAyB,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,2BAA2B,EAAE,IAAI,CAC5C,eAAe,EACf,eAAe,GAAG,aAAa,CAIhC,CAAC;AAEF,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,YAAY,GAAG,aAAa,GAAG,cAAc,CAAC,GAC3E,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,eAAe,EACvB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAGT;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,CAWjE;AAED,6EAA6E;AAC7E,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,4BAA4B,GAAG,IAAI,GAAG,SAAS,GACpD,OAAO,CAiBT;AAYD,6FAA6F;AAC7F,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC,CAiDjF;AAED,oFAAoF;AACpF,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,eAAe,CAAC,CAa1B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { loadMaysonEnvConfig } from './config.js';
|
|
2
|
+
/** When metadata record does not exist yet — show pill and allow remix. */
|
|
3
|
+
export const METADATA_NOT_FOUND_DEFAULTS = {
|
|
4
|
+
showRemixPill: true,
|
|
5
|
+
enableRemix: true,
|
|
6
|
+
};
|
|
7
|
+
export function resolveGeneratedAppMetadataUrl(config) {
|
|
8
|
+
const base = config.apiBaseUrl.replace(/\/$/, '');
|
|
9
|
+
const workspaceId = config.workspaceId.trim();
|
|
10
|
+
const collectionId = config.collectionId.trim();
|
|
11
|
+
if (!base || !workspaceId || !collectionId) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return `${base}/sigma/web/v1/generated-app-metadata/workspaces/${encodeURIComponent(workspaceId)}/collections/${encodeURIComponent(collectionId)}`;
|
|
15
|
+
}
|
|
16
|
+
export function shouldFetchGeneratedAppMetadata(config, webToken) {
|
|
17
|
+
const token = (webToken ?? config.webToken).trim();
|
|
18
|
+
return resolveGeneratedAppMetadataUrl(config) !== null && token.length > 0;
|
|
19
|
+
}
|
|
20
|
+
export function mapGeneratedAppMetadataValue(value) {
|
|
21
|
+
const partial = {};
|
|
22
|
+
if (value.show_remix_pill !== null && value.show_remix_pill !== undefined) {
|
|
23
|
+
partial.showRemixPill = value.show_remix_pill;
|
|
24
|
+
}
|
|
25
|
+
if (value.enable_remix !== null && value.enable_remix !== undefined) {
|
|
26
|
+
partial.enableRemix = value.enable_remix;
|
|
27
|
+
}
|
|
28
|
+
return partial;
|
|
29
|
+
}
|
|
30
|
+
/** True when API indicates no metadata row exists yet (HTTP or body 404). */
|
|
31
|
+
export function isGeneratedAppMetadataNotFound(httpStatus, data) {
|
|
32
|
+
if (httpStatus === 404) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (!data || typeof data !== 'object') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (data.response_code === 404) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (data.response_type === 'failed' &&
|
|
42
|
+
/generated app metadata not found/i.test(data.message ?? '')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function readJsonSafe(response) {
|
|
48
|
+
try {
|
|
49
|
+
return (await response.json());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Fetch generated-app-metadata. Returns null on hard failure (network / non-404 errors). */
|
|
56
|
+
export async function fetchGeneratedAppMetadata(config, options = {}) {
|
|
57
|
+
const url = resolveGeneratedAppMetadataUrl(config);
|
|
58
|
+
const token = (options.webToken ?? config.webToken).trim();
|
|
59
|
+
if (!url || !token) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
63
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetchImpl(url, {
|
|
66
|
+
headers: {
|
|
67
|
+
Accept: 'application/json',
|
|
68
|
+
'M-Web-Token': token,
|
|
69
|
+
},
|
|
70
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
71
|
+
});
|
|
72
|
+
const data = await readJsonSafe(response);
|
|
73
|
+
if (isGeneratedAppMetadataNotFound(response.status, data)) {
|
|
74
|
+
console.log('[mayson] generated-app-metadata not found — defaulting show_remix_pill=true, enable_remix=true');
|
|
75
|
+
return { ...METADATA_NOT_FOUND_DEFAULTS };
|
|
76
|
+
}
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
console.warn(`[mayson] generated-app-metadata request failed (${response.status}) — using local config`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!data?.value || typeof data.value !== 'object') {
|
|
82
|
+
console.warn('[mayson] generated-app-metadata response invalid — using local config');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return mapGeneratedAppMetadataValue(data.value);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.warn(`[mayson] generated-app-metadata fetch failed: ${err.message} — using local config`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Local env config merged with generated-app-metadata when credentials are set. */
|
|
93
|
+
export async function resolveMaysonConfig(options = {}) {
|
|
94
|
+
const local = loadMaysonEnvConfig();
|
|
95
|
+
if (!shouldFetchGeneratedAppMetadata(local, options.webToken)) {
|
|
96
|
+
return local;
|
|
97
|
+
}
|
|
98
|
+
const api = await fetchGeneratedAppMetadata(local, options);
|
|
99
|
+
if (!api) {
|
|
100
|
+
return local;
|
|
101
|
+
}
|
|
102
|
+
return { ...local, ...api };
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** UTM source for remix links opened from the watermark pill. */
|
|
2
|
+
export declare const BADGE_UTM_SOURCE = "mayson-badge";
|
|
3
|
+
export type RemixUrlConfig = {
|
|
4
|
+
appBaseUrl?: string;
|
|
5
|
+
collectionId?: string;
|
|
6
|
+
enableRemix?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build the mayson.dev remix redirect URL (same pattern as the legacy build plugins).
|
|
10
|
+
* Baked into the generated watermark component at inject time.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildRemixRedirectUrl(config?: RemixUrlConfig): string;
|
|
13
|
+
//# sourceMappingURL=remix-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remix-url.d.ts","sourceRoot":"","sources":["../../src/shared/remix-url.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,cAAmB,GAAG,MAAM,CAWzE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** UTM source for remix links opened from the watermark pill. */
|
|
2
|
+
export const BADGE_UTM_SOURCE = 'mayson-badge';
|
|
3
|
+
/**
|
|
4
|
+
* Build the mayson.dev remix redirect URL (same pattern as the legacy build plugins).
|
|
5
|
+
* Baked into the generated watermark component at inject time.
|
|
6
|
+
*/
|
|
7
|
+
export function buildRemixRedirectUrl(config = {}) {
|
|
8
|
+
const base = (config.appBaseUrl ?? 'https://mayson.dev').replace(/\/$/, '') || 'https://mayson.dev';
|
|
9
|
+
const collectionId = config.collectionId?.trim() ?? '';
|
|
10
|
+
const enableRemix = config.enableRemix === true;
|
|
11
|
+
if (enableRemix && collectionId) {
|
|
12
|
+
const remixPath = `/workspaces?coll=${collectionId}&utm_source=${BADGE_UTM_SOURCE}&action=remix`;
|
|
13
|
+
return `${base}/redirect?to=${encodeURIComponent(remixPath)}`;
|
|
14
|
+
}
|
|
15
|
+
return `${base}/`;
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mayson-org/inject-script",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "CLI and library to locate Next.js App Router root layouts, generate Mayson plugin components, and inject them into the layout tree.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
|
-
"module": "./dist/index.mjs",
|
|
7
6
|
"types": "./dist/index.d.ts",
|
|
8
7
|
"type": "module",
|
|
9
8
|
"bin": {
|
|
@@ -12,7 +11,7 @@
|
|
|
12
11
|
"exports": {
|
|
13
12
|
".": {
|
|
14
13
|
"types": "./dist/index.d.ts",
|
|
15
|
-
"import": "./dist/index.
|
|
14
|
+
"import": "./dist/index.js",
|
|
16
15
|
"default": "./dist/index.js"
|
|
17
16
|
}
|
|
18
17
|
},
|
|
@@ -25,8 +24,8 @@
|
|
|
25
24
|
"files": [
|
|
26
25
|
"bin",
|
|
27
26
|
"dist",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
27
|
+
"README.md",
|
|
28
|
+
".env.example"
|
|
30
29
|
],
|
|
31
30
|
"publishConfig": {
|
|
32
31
|
"access": "public"
|
|
@@ -35,10 +34,18 @@
|
|
|
35
34
|
"nextjs",
|
|
36
35
|
"app-router",
|
|
37
36
|
"layout",
|
|
38
|
-
"
|
|
37
|
+
"watermark",
|
|
38
|
+
"metadata",
|
|
39
39
|
"mayson",
|
|
40
40
|
"cli"
|
|
41
41
|
],
|
|
42
42
|
"author": "",
|
|
43
|
-
"license": "MIT"
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^26.6.1",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
44
51
|
}
|
package/src/find-root-layout.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const LAYOUT_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Recursively search a directory for layout files.
|
|
8
|
-
*/
|
|
9
|
-
function findLayoutFiles(dir: string, fileList: string[] = []): string[] {
|
|
10
|
-
if (!fs.existsSync(dir)) return fileList;
|
|
11
|
-
|
|
12
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
13
|
-
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
const fullPath = path.join(dir, entry.name);
|
|
16
|
-
|
|
17
|
-
if (entry.isDirectory()) {
|
|
18
|
-
// Don't traverse node_modules or hidden folders (.next, .git)
|
|
19
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
findLayoutFiles(fullPath, fileList);
|
|
23
|
-
} else if (entry.isFile()) {
|
|
24
|
-
const ext = path.extname(entry.name);
|
|
25
|
-
const base = path.basename(entry.name, ext);
|
|
26
|
-
if (base === 'layout' && LAYOUT_EXTENSIONS.includes(ext)) {
|
|
27
|
-
fileList.push(fullPath);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return fileList;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Scores a layout file candidate to determine if it is the root layout.
|
|
37
|
-
* Higher score = higher likelihood of being the root layout.
|
|
38
|
-
*/
|
|
39
|
-
function scoreLayoutCandidate(filePath: string, projectRoot: string): number {
|
|
40
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
42
|
-
const depth = relativePath.split(path.sep).length;
|
|
43
|
-
|
|
44
|
-
let score = 100 - depth; // Shorter path depth is preferred
|
|
45
|
-
|
|
46
|
-
// Check for HTML/Body root tags
|
|
47
|
-
if (/<html/i.test(content)) score += 50;
|
|
48
|
-
if (/<body/i.test(content)) score += 50;
|
|
49
|
-
|
|
50
|
-
// Check if located directly under app/ or src/app/
|
|
51
|
-
const isDirectAppChild = /^src[/\\]app[/\\]layout\.[jt]sx?$/.test(relativePath) ||
|
|
52
|
-
/^app[/\\]layout\.[jt]sx?$/.test(relativePath);
|
|
53
|
-
if (isDirectAppChild) score += 40;
|
|
54
|
-
|
|
55
|
-
return score;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Finds the most likely Next.js App Router root layout file in a project.
|
|
60
|
-
*/
|
|
61
|
-
export function findRootLayout(projectRoot: string = process.cwd()): string | null {
|
|
62
|
-
const possibleAppDirs = [
|
|
63
|
-
path.join(projectRoot, 'src', 'app'),
|
|
64
|
-
path.join(projectRoot, 'app'),
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
const candidateFiles: string[] = [];
|
|
68
|
-
|
|
69
|
-
for (const appDir of possibleAppDirs) {
|
|
70
|
-
if (fs.existsSync(appDir)) {
|
|
71
|
-
findLayoutFiles(appDir, candidateFiles);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (candidateFiles.length === 0) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Score each candidate to pick the true root layout
|
|
80
|
-
const scored = candidateFiles.map((filePath) => ({
|
|
81
|
-
filePath,
|
|
82
|
-
score: scoreLayoutCandidate(filePath, projectRoot),
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
scored.sort((a, b) => b.score - a.score);
|
|
86
|
-
|
|
87
|
-
return scored[0]?.filePath || null;
|
|
88
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { findRootLayout } from './find-root-layout.js';
|
|
2
|
-
import { injectMaysonIframe, InjectorOptions, InjectorResult } from './injector.js';
|
|
3
|
-
|
|
4
|
-
export { findRootLayout, injectMaysonIframe };
|
|
5
|
-
export type { InjectorOptions, InjectorResult };
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Convenience function to automatically locate and inject the Mayson iframe into a Next.js project.
|
|
9
|
-
*/
|
|
10
|
-
export function runAutoInject(projectRoot: string = process.cwd(), iframeUrl: string = 'https://mayson.dev/', dryRun: boolean = false): InjectorResult {
|
|
11
|
-
const layoutPath = findRootLayout(projectRoot);
|
|
12
|
-
|
|
13
|
-
if (!layoutPath) {
|
|
14
|
-
return {
|
|
15
|
-
success: false,
|
|
16
|
-
filePath: '',
|
|
17
|
-
alreadyInjected: false,
|
|
18
|
-
error: `Could not locate Next.js App Router root layout file in ${projectRoot}`,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return injectMaysonIframe({
|
|
23
|
-
filePath: layoutPath,
|
|
24
|
-
iframeUrl,
|
|
25
|
-
dryRun,
|
|
26
|
-
});
|
|
27
|
-
}
|
package/src/injector.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
|
-
export interface InjectorOptions {
|
|
4
|
-
filePath: string;
|
|
5
|
-
iframeUrl?: string;
|
|
6
|
-
dryRun?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface InjectorResult {
|
|
10
|
-
success: boolean;
|
|
11
|
-
filePath: string;
|
|
12
|
-
alreadyInjected: boolean;
|
|
13
|
-
modifiedContent?: string;
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const INJECTION_MARKER = '/* @mayson-iframe-injected */';
|
|
18
|
-
|
|
19
|
-
export function injectMaysonIframe(options: InjectorOptions): InjectorResult {
|
|
20
|
-
const { filePath, iframeUrl = 'https://mayson.dev/', dryRun = false } = options;
|
|
21
|
-
|
|
22
|
-
if (!fs.existsSync(filePath)) {
|
|
23
|
-
return {
|
|
24
|
-
success: false,
|
|
25
|
-
filePath,
|
|
26
|
-
alreadyInjected: false,
|
|
27
|
-
error: `File not found: ${filePath}`,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
32
|
-
|
|
33
|
-
// Idempotency check: Don't inject twice
|
|
34
|
-
if (content.includes(INJECTION_MARKER) || content.includes(iframeUrl)) {
|
|
35
|
-
return {
|
|
36
|
-
success: true,
|
|
37
|
-
filePath,
|
|
38
|
-
alreadyInjected: true,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const snippet = `
|
|
43
|
-
{${INJECTION_MARKER}}
|
|
44
|
-
<iframe
|
|
45
|
-
src="${iframeUrl}"
|
|
46
|
-
style={{
|
|
47
|
-
position: 'fixed',
|
|
48
|
-
bottom: '20px',
|
|
49
|
-
right: '20px',
|
|
50
|
-
width: '400px',
|
|
51
|
-
height: '600px',
|
|
52
|
-
border: 'none',
|
|
53
|
-
borderRadius: '12px',
|
|
54
|
-
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.25)',
|
|
55
|
-
zIndex: 999999,
|
|
56
|
-
}}
|
|
57
|
-
title="Mayson Dev Overlay"
|
|
58
|
-
/>`;
|
|
59
|
-
|
|
60
|
-
let updatedContent: string | null = null;
|
|
61
|
-
|
|
62
|
-
// Option 1: Inject before </body>
|
|
63
|
-
if (/<\/body>/i.test(content)) {
|
|
64
|
-
updatedContent = content.replace(/<\/body>/i, `${snippet}\n </body>`);
|
|
65
|
-
}
|
|
66
|
-
// Option 2: Inject before </html>
|
|
67
|
-
else if (/<\/html>/i.test(content)) {
|
|
68
|
-
updatedContent = content.replace(/<\/html>/i, `${snippet}\n </html>`);
|
|
69
|
-
}
|
|
70
|
-
// Option 3: Inject before the closing tag of the return block (e.g. </main> or </div>)
|
|
71
|
-
else {
|
|
72
|
-
const lastClosingTagIndex = content.lastIndexOf('</');
|
|
73
|
-
if (lastClosingTagIndex !== -1) {
|
|
74
|
-
updatedContent =
|
|
75
|
-
content.slice(0, lastClosingTagIndex) +
|
|
76
|
-
snippet +
|
|
77
|
-
'\n ' +
|
|
78
|
-
content.slice(lastClosingTagIndex);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!updatedContent) {
|
|
83
|
-
return {
|
|
84
|
-
success: false,
|
|
85
|
-
filePath,
|
|
86
|
-
alreadyInjected: false,
|
|
87
|
-
error: 'Could not find suitable JSX insertion point in layout file.',
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!dryRun) {
|
|
92
|
-
fs.writeFileSync(filePath, updatedContent, 'utf-8');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
success: true,
|
|
97
|
-
filePath,
|
|
98
|
-
alreadyInjected: false,
|
|
99
|
-
modifiedContent: updatedContent,
|
|
100
|
-
};
|
|
101
|
-
}
|