@mayson-org/inject-script 1.0.7 → 1.0.9
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 +4 -1
- package/README.md +33 -7
- package/assets/pwa/mayson-logo-dark-192x192.png +0 -0
- package/assets/pwa/mayson-logo-dark-512x512-maskable.png +0 -0
- package/assets/pwa/mayson-logo-dark-512x512.png +0 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -3
- package/dist/core/generators.d.ts.map +1 -1
- package/dist/core/generators.js +5 -4
- package/dist/core/remove-component.d.ts +8 -0
- package/dist/core/remove-component.d.ts.map +1 -0
- package/dist/core/remove-component.js +44 -0
- package/dist/core/types.d.ts +26 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -22
- package/dist/plugins/index.d.ts +2 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/pwa/index.d.ts +9 -0
- package/dist/plugins/pwa/index.d.ts.map +1 -0
- package/dist/plugins/pwa/index.js +11 -0
- package/dist/plugins/pwa/inject-project.d.ts +4 -0
- package/dist/plugins/pwa/inject-project.d.ts.map +1 -0
- package/dist/plugins/pwa/inject-project.js +169 -0
- package/dist/plugins/pwa/patch-layout-metadata.d.ts +14 -0
- package/dist/plugins/pwa/patch-layout-metadata.d.ts.map +1 -0
- package/dist/plugins/pwa/patch-layout-metadata.js +162 -0
- package/dist/plugins/pwa/templates/install-prompt.d.ts +3 -0
- package/dist/plugins/pwa/templates/install-prompt.d.ts.map +1 -0
- package/dist/plugins/pwa/templates/install-prompt.js +186 -0
- package/dist/plugins/pwa/templates/manifest.d.ts +11 -0
- package/dist/plugins/pwa/templates/manifest.d.ts.map +1 -0
- package/dist/plugins/pwa/templates/manifest.js +44 -0
- package/dist/plugins/pwa/templates/service-worker-register.d.ts +3 -0
- package/dist/plugins/pwa/templates/service-worker-register.d.ts.map +1 -0
- package/dist/plugins/pwa/templates/service-worker-register.js +23 -0
- package/dist/plugins/pwa/templates/sw.js.d.ts +6 -0
- package/dist/plugins/pwa/templates/sw.js.d.ts.map +1 -0
- package/dist/plugins/pwa/templates/sw.js.js +55 -0
- package/dist/shared/config.d.ts +2 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +2 -0
- package/dist/shared/generated-app-metadata.d.ts +3 -2
- package/dist/shared/generated-app-metadata.d.ts.map +1 -1
- package/dist/shared/generated-app-metadata.js +24 -5
- package/package.json +4 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { injectComponentsIntoLayout } from '../../core/injector.js';
|
|
5
|
+
import { patchLayoutPwaMetadata } from './patch-layout-metadata.js';
|
|
6
|
+
import { buildManifestSource, MANIFEST_MARKER, } from './templates/manifest.js';
|
|
7
|
+
import { buildMaysonInstallPromptSource } from './templates/install-prompt.js';
|
|
8
|
+
import { buildMaysonServiceWorkerRegisterSource } from './templates/service-worker-register.js';
|
|
9
|
+
import { buildSwJsSource, SW_MARKER } from './templates/sw.js.js';
|
|
10
|
+
const DEFAULT_BRANDING = {
|
|
11
|
+
appName: 'My App',
|
|
12
|
+
themeColor: '#0a0a0a',
|
|
13
|
+
backgroundColor: '#ffffff',
|
|
14
|
+
description: 'Generated by Mayson',
|
|
15
|
+
};
|
|
16
|
+
const ICON_FILES = [
|
|
17
|
+
'mayson-logo-dark-192x192.png',
|
|
18
|
+
'mayson-logo-dark-512x512.png',
|
|
19
|
+
'mayson-logo-dark-512x512-maskable.png',
|
|
20
|
+
];
|
|
21
|
+
function resolvePwaAssetsDir() {
|
|
22
|
+
// dist/plugins/pwa → ../../../assets/pwa (package root)
|
|
23
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
return path.resolve(here, '../../../assets/pwa');
|
|
25
|
+
}
|
|
26
|
+
function resolveBranding(ctx) {
|
|
27
|
+
const pwa = ctx.context?.pwa;
|
|
28
|
+
return {
|
|
29
|
+
appName: pwa?.appName?.trim() || DEFAULT_BRANDING.appName,
|
|
30
|
+
themeColor: pwa?.themeColor?.trim() || DEFAULT_BRANDING.themeColor,
|
|
31
|
+
backgroundColor: pwa?.backgroundColor?.trim() || DEFAULT_BRANDING.backgroundColor,
|
|
32
|
+
description: pwa?.description?.trim() || DEFAULT_BRANDING.description,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function writeMarkedFile(params) {
|
|
36
|
+
const { filePath, source, marker, dryRun, label } = params;
|
|
37
|
+
if (fs.existsSync(filePath)) {
|
|
38
|
+
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
if (!existing.includes(marker)) {
|
|
40
|
+
return `skipped ${label} — already exists without ${marker} marker`;
|
|
41
|
+
}
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
return `would refresh ${label}`;
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(filePath, source, 'utf-8');
|
|
46
|
+
return `refreshed ${label}`;
|
|
47
|
+
}
|
|
48
|
+
if (dryRun) {
|
|
49
|
+
return `would write ${label}`;
|
|
50
|
+
}
|
|
51
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(filePath, source, 'utf-8');
|
|
53
|
+
return `wrote ${label}`;
|
|
54
|
+
}
|
|
55
|
+
function copyDefaultIcons(projectRoot, dryRun) {
|
|
56
|
+
const messages = [];
|
|
57
|
+
const assetsDir = resolvePwaAssetsDir();
|
|
58
|
+
const iconsDir = path.join(projectRoot, 'public', 'icons');
|
|
59
|
+
for (const file of ICON_FILES) {
|
|
60
|
+
const dest = path.join(iconsDir, file);
|
|
61
|
+
const src = path.join(assetsDir, file);
|
|
62
|
+
if (fs.existsSync(dest)) {
|
|
63
|
+
messages.push(`skipped icon ${file} — already present`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!fs.existsSync(src)) {
|
|
67
|
+
messages.push(`missing packaged icon ${file} at ${src}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
messages.push(`would copy icon ${file}`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
fs.mkdirSync(iconsDir, { recursive: true });
|
|
75
|
+
fs.copyFileSync(src, dest);
|
|
76
|
+
messages.push(`copied icon ${file}`);
|
|
77
|
+
}
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
80
|
+
function writePwaComponents(ctx) {
|
|
81
|
+
const { layoutPath, dryRun = false, isTypeScript = true } = ctx;
|
|
82
|
+
const layoutDir = path.dirname(layoutPath);
|
|
83
|
+
const ext = isTypeScript ? '.tsx' : '.jsx';
|
|
84
|
+
const messages = [];
|
|
85
|
+
const components = [];
|
|
86
|
+
const files = [
|
|
87
|
+
{
|
|
88
|
+
name: 'MaysonServiceWorkerRegister',
|
|
89
|
+
source: buildMaysonServiceWorkerRegisterSource(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'MaysonInstallPrompt',
|
|
93
|
+
source: buildMaysonInstallPromptSource(),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const filePath = path.join(layoutDir, `${file.name}${ext}`);
|
|
98
|
+
if (!dryRun) {
|
|
99
|
+
fs.writeFileSync(filePath, file.source, 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
messages.push(dryRun ? `would write ${file.name}${ext}` : `wrote ${file.name}${ext}`);
|
|
102
|
+
components.push({
|
|
103
|
+
name: file.name,
|
|
104
|
+
filePath,
|
|
105
|
+
importStatement: `import { ${file.name} } from './${file.name}';`,
|
|
106
|
+
jsxTag: `<${file.name} />`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return { components, messages };
|
|
110
|
+
}
|
|
111
|
+
/** Orchestrate all PWA project-level writes + layout inject for SW/install components. */
|
|
112
|
+
export function injectPwaProject(ctx) {
|
|
113
|
+
const messages = [];
|
|
114
|
+
const injectedComponents = [];
|
|
115
|
+
const skippedComponents = [];
|
|
116
|
+
const { projectRoot, layoutPath, dryRun = false } = ctx;
|
|
117
|
+
const branding = resolveBranding(ctx);
|
|
118
|
+
const appDir = path.dirname(layoutPath);
|
|
119
|
+
// 1. Components next to layout + inject into layout
|
|
120
|
+
const { components, messages: compMessages } = writePwaComponents(ctx);
|
|
121
|
+
messages.push(...compMessages);
|
|
122
|
+
const injectResult = injectComponentsIntoLayout({
|
|
123
|
+
filePath: layoutPath,
|
|
124
|
+
components,
|
|
125
|
+
dryRun,
|
|
126
|
+
});
|
|
127
|
+
injectedComponents.push(...injectResult.injectedComponents);
|
|
128
|
+
skippedComponents.push(...injectResult.skippedComponents);
|
|
129
|
+
if (injectResult.error) {
|
|
130
|
+
messages.push(`layout inject error: ${injectResult.error}`);
|
|
131
|
+
}
|
|
132
|
+
// 2. app/manifest.ts
|
|
133
|
+
messages.push(writeMarkedFile({
|
|
134
|
+
filePath: path.join(appDir, 'manifest.ts'),
|
|
135
|
+
source: buildManifestSource(branding),
|
|
136
|
+
marker: MANIFEST_MARKER,
|
|
137
|
+
dryRun,
|
|
138
|
+
label: 'app/manifest.ts',
|
|
139
|
+
}));
|
|
140
|
+
// 3. public/sw.js
|
|
141
|
+
messages.push(writeMarkedFile({
|
|
142
|
+
filePath: path.join(projectRoot, 'public', 'sw.js'),
|
|
143
|
+
source: buildSwJsSource(),
|
|
144
|
+
marker: SW_MARKER,
|
|
145
|
+
dryRun,
|
|
146
|
+
label: 'public/sw.js',
|
|
147
|
+
}));
|
|
148
|
+
// 4. Icons (never overwrite)
|
|
149
|
+
messages.push(...copyDefaultIcons(projectRoot, dryRun));
|
|
150
|
+
// 5. Patch layout metadata (appleWebApp + themeColor)
|
|
151
|
+
if (fs.existsSync(layoutPath)) {
|
|
152
|
+
// Re-read after component inject may have written the file
|
|
153
|
+
const layoutContent = dryRun
|
|
154
|
+
? fs.readFileSync(layoutPath, 'utf-8')
|
|
155
|
+
: fs.readFileSync(layoutPath, 'utf-8');
|
|
156
|
+
const patched = patchLayoutPwaMetadata(layoutContent, {
|
|
157
|
+
appName: branding.appName,
|
|
158
|
+
themeColor: branding.themeColor,
|
|
159
|
+
});
|
|
160
|
+
messages.push(...patched.messages);
|
|
161
|
+
if (patched.changed && !dryRun) {
|
|
162
|
+
fs.writeFileSync(layoutPath, patched.content, 'utf-8');
|
|
163
|
+
}
|
|
164
|
+
else if (patched.changed && dryRun) {
|
|
165
|
+
messages.push('would patch layout PWA metadata');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { messages, injectedComponents, skippedComponents };
|
|
169
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PwaLayoutBranding = {
|
|
2
|
+
appName: string;
|
|
3
|
+
themeColor: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Insert appleWebApp / viewport.themeColor when absent.
|
|
7
|
+
* Does not rewrite existing custom appleWebApp or themeColor values.
|
|
8
|
+
*/
|
|
9
|
+
export declare function patchLayoutPwaMetadata(content: string, branding: PwaLayoutBranding): {
|
|
10
|
+
content: string;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
messages: string[];
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=patch-layout-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch-layout-metadata.d.ts","sourceRoot":"","sources":["../../../src/plugins/pwa/patch-layout-metadata.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,iBAAiB,GAC1B;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkC3D"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insert appleWebApp / viewport.themeColor when absent.
|
|
3
|
+
* Does not rewrite existing custom appleWebApp or themeColor values.
|
|
4
|
+
*/
|
|
5
|
+
export function patchLayoutPwaMetadata(content, branding) {
|
|
6
|
+
let next = content;
|
|
7
|
+
const messages = [];
|
|
8
|
+
let changed = false;
|
|
9
|
+
if (!/\bappleWebApp\s*:/.test(next)) {
|
|
10
|
+
const patched = insertAppleWebApp(next, branding.appName);
|
|
11
|
+
if (patched !== next) {
|
|
12
|
+
next = patched;
|
|
13
|
+
changed = true;
|
|
14
|
+
messages.push('added appleWebApp to layout metadata');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
messages.push('layout already has appleWebApp — left unchanged');
|
|
19
|
+
}
|
|
20
|
+
if (!/\bthemeColor\s*:/.test(next)) {
|
|
21
|
+
const patched = insertThemeColor(next, branding.themeColor);
|
|
22
|
+
if (patched !== next) {
|
|
23
|
+
next = patched;
|
|
24
|
+
changed = true;
|
|
25
|
+
messages.push('added viewport.themeColor to layout');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
messages.push('layout already has themeColor — left unchanged');
|
|
30
|
+
}
|
|
31
|
+
// Ensure Viewport type import when we added/need viewport export
|
|
32
|
+
if (/\bexport\s+const\s+viewport\b/.test(next) && !/\bViewport\b/.test(next)) {
|
|
33
|
+
next = ensureViewportImport(next);
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
return { content: next, changed, messages };
|
|
37
|
+
}
|
|
38
|
+
function insertAppleWebApp(content, appName) {
|
|
39
|
+
const appleBlock = ` appleWebApp: {
|
|
40
|
+
capable: true,
|
|
41
|
+
title: ${JSON.stringify(appName)},
|
|
42
|
+
statusBarStyle: 'default',
|
|
43
|
+
},`;
|
|
44
|
+
// Prefer inserting inside an existing `export const metadata = { ... }` object
|
|
45
|
+
const metadataMatch = content.match(/export\s+const\s+metadata(?:\s*:\s*[^=]+)?\s*=\s*\{/);
|
|
46
|
+
if (metadataMatch && metadataMatch.index !== undefined) {
|
|
47
|
+
const openBrace = metadataMatch.index + metadataMatch[0].length - 1;
|
|
48
|
+
const closeBrace = findMatchingBrace(content, openBrace);
|
|
49
|
+
if (closeBrace !== -1) {
|
|
50
|
+
const before = content.slice(0, closeBrace);
|
|
51
|
+
const needsComma = /,\s*$/.test(before.trimEnd()) ? '' : ',';
|
|
52
|
+
return (before.trimEnd() +
|
|
53
|
+
needsComma +
|
|
54
|
+
'\n' +
|
|
55
|
+
appleBlock +
|
|
56
|
+
'\n' +
|
|
57
|
+
content.slice(closeBrace));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// No metadata export — prepend a minimal one
|
|
61
|
+
const importEnd = findLastImportEnd(content);
|
|
62
|
+
const block = `export const metadata = {
|
|
63
|
+
${appleBlock}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
if (importEnd === 0) {
|
|
68
|
+
return block + content;
|
|
69
|
+
}
|
|
70
|
+
return content.slice(0, importEnd) + '\n\n' + block + content.slice(importEnd);
|
|
71
|
+
}
|
|
72
|
+
function insertThemeColor(content, themeColor) {
|
|
73
|
+
if (/\bexport\s+const\s+viewport\b/.test(content)) {
|
|
74
|
+
const viewportMatch = content.match(/export\s+const\s+viewport(?:\s*:\s*[^=]+)?\s*=\s*\{/);
|
|
75
|
+
if (viewportMatch && viewportMatch.index !== undefined) {
|
|
76
|
+
const openBrace = viewportMatch.index + viewportMatch[0].length - 1;
|
|
77
|
+
const closeBrace = findMatchingBrace(content, openBrace);
|
|
78
|
+
if (closeBrace !== -1) {
|
|
79
|
+
const before = content.slice(0, closeBrace);
|
|
80
|
+
const needsComma = /,\s*$/.test(before.trimEnd()) ? '' : ',';
|
|
81
|
+
const line = ` themeColor: ${JSON.stringify(themeColor)},`;
|
|
82
|
+
return (before.trimEnd() +
|
|
83
|
+
needsComma +
|
|
84
|
+
'\n' +
|
|
85
|
+
line +
|
|
86
|
+
'\n' +
|
|
87
|
+
content.slice(closeBrace));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
const block = `export const viewport = {
|
|
93
|
+
themeColor: ${JSON.stringify(themeColor)},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
`;
|
|
97
|
+
// Place after metadata export if present
|
|
98
|
+
const metadataEnd = findExportObjectEnd(content, 'metadata');
|
|
99
|
+
if (metadataEnd !== -1) {
|
|
100
|
+
return content.slice(0, metadataEnd) + '\n\n' + block + content.slice(metadataEnd);
|
|
101
|
+
}
|
|
102
|
+
const importEnd = findLastImportEnd(content);
|
|
103
|
+
if (importEnd === 0) {
|
|
104
|
+
return block + content;
|
|
105
|
+
}
|
|
106
|
+
return content.slice(0, importEnd) + '\n\n' + block + content.slice(importEnd);
|
|
107
|
+
}
|
|
108
|
+
function ensureViewportImport(content) {
|
|
109
|
+
const fromNext = content.match(/import\s+type\s*\{([^}]*)\}\s*from\s*['"]next['"]/);
|
|
110
|
+
if (fromNext) {
|
|
111
|
+
const names = fromNext[1];
|
|
112
|
+
if (/\bViewport\b/.test(names)) {
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
const updated = names.trim().length
|
|
116
|
+
? `${names.trim()}, Viewport`
|
|
117
|
+
: 'Viewport';
|
|
118
|
+
return content.replace(fromNext[0], `import type { ${updated} } from 'next'`);
|
|
119
|
+
}
|
|
120
|
+
// Combined value+type import: import { type Metadata, type Viewport } from 'next'
|
|
121
|
+
const mixed = content.match(/import\s*\{([^}]*)\}\s*from\s*['"]next['"]/);
|
|
122
|
+
if (mixed && /\bMetadata\b/.test(mixed[1]) && !/\bViewport\b/.test(mixed[1])) {
|
|
123
|
+
return content.replace(mixed[0], `import {${mixed[1]}, type Viewport} from 'next'`);
|
|
124
|
+
}
|
|
125
|
+
return `import type { Viewport } from 'next';\n` + content;
|
|
126
|
+
}
|
|
127
|
+
function findMatchingBrace(source, openIndex) {
|
|
128
|
+
let depth = 0;
|
|
129
|
+
for (let i = openIndex; i < source.length; i++) {
|
|
130
|
+
const ch = source[i];
|
|
131
|
+
if (ch === '{')
|
|
132
|
+
depth++;
|
|
133
|
+
else if (ch === '}') {
|
|
134
|
+
depth--;
|
|
135
|
+
if (depth === 0)
|
|
136
|
+
return i;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return -1;
|
|
140
|
+
}
|
|
141
|
+
function findLastImportEnd(content) {
|
|
142
|
+
const matches = Array.from(content.matchAll(/^import\s.+?;$/gm));
|
|
143
|
+
if (matches.length === 0)
|
|
144
|
+
return 0;
|
|
145
|
+
const last = matches[matches.length - 1];
|
|
146
|
+
return (last.index ?? 0) + last[0].length;
|
|
147
|
+
}
|
|
148
|
+
function findExportObjectEnd(content, name) {
|
|
149
|
+
const re = new RegExp(`export\\s+const\\s+${name}(?:\\s*:\\s*[^=]+)?\\s*=\\s*\\{`);
|
|
150
|
+
const match = content.match(re);
|
|
151
|
+
if (!match || match.index === undefined)
|
|
152
|
+
return -1;
|
|
153
|
+
const openBrace = match.index + match[0].length - 1;
|
|
154
|
+
const close = findMatchingBrace(content, openBrace);
|
|
155
|
+
if (close === -1)
|
|
156
|
+
return -1;
|
|
157
|
+
// Include trailing semicolon if present
|
|
158
|
+
let end = close + 1;
|
|
159
|
+
if (content[end] === ';')
|
|
160
|
+
end++;
|
|
161
|
+
return end;
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-prompt.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/install-prompt.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,wBAAgB,8BAA8B,IAAI,MAAM,CAwLvD"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** Source for MaysonInstallPrompt — inline styles, host-shell ?pwa_install=1 contract. */
|
|
2
|
+
export function buildMaysonInstallPromptSource() {
|
|
3
|
+
return `'use client';
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
6
|
+
|
|
7
|
+
const DISMISS_KEY = 'mayson-pwa-install-dismissed';
|
|
8
|
+
|
|
9
|
+
interface BeforeInstallPromptEvent extends Event {
|
|
10
|
+
prompt: () => Promise<void>;
|
|
11
|
+
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isIOS(): boolean {
|
|
15
|
+
if (typeof navigator === 'undefined') return false;
|
|
16
|
+
return /iphone|ipad|ipod/i.test(navigator.userAgent);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isStandalone(): boolean {
|
|
20
|
+
if (typeof window === 'undefined') return false;
|
|
21
|
+
return (
|
|
22
|
+
window.matchMedia('(display-mode: standalone)').matches ||
|
|
23
|
+
(navigator as Navigator & { standalone?: boolean }).standalone === true
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function MaysonInstallPrompt() {
|
|
28
|
+
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(null);
|
|
29
|
+
const [visible, setVisible] = useState(false);
|
|
30
|
+
const [showIOSHint, setShowIOSHint] = useState(false);
|
|
31
|
+
const [showBrowserHint, setShowBrowserHint] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const params = new URLSearchParams(window.location.search);
|
|
35
|
+
const forced = params.get('pwa_install') === '1';
|
|
36
|
+
if (forced) {
|
|
37
|
+
params.delete('pwa_install');
|
|
38
|
+
const qs = params.toString();
|
|
39
|
+
history.replaceState(
|
|
40
|
+
null,
|
|
41
|
+
'',
|
|
42
|
+
window.location.pathname + (qs ? \`?\${qs}\` : '') + window.location.hash,
|
|
43
|
+
);
|
|
44
|
+
setVisible(true);
|
|
45
|
+
} else if (isStandalone() || localStorage.getItem(DISMISS_KEY) === '1') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onPrompt = (e: Event) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
setDeferred(e as BeforeInstallPromptEvent);
|
|
52
|
+
setVisible(true);
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener('beforeinstallprompt', onPrompt);
|
|
55
|
+
|
|
56
|
+
if (isIOS()) setVisible(true);
|
|
57
|
+
|
|
58
|
+
return () => window.removeEventListener('beforeinstallprompt', onPrompt);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
if (!visible) return null;
|
|
62
|
+
|
|
63
|
+
const dismiss = () => {
|
|
64
|
+
localStorage.setItem(DISMISS_KEY, '1');
|
|
65
|
+
setVisible(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const install = async () => {
|
|
69
|
+
if (deferred) {
|
|
70
|
+
await deferred.prompt();
|
|
71
|
+
const { outcome } = await deferred.userChoice;
|
|
72
|
+
if (outcome === 'accepted') setVisible(false);
|
|
73
|
+
setDeferred(null);
|
|
74
|
+
} else if (isIOS()) {
|
|
75
|
+
setShowIOSHint(true);
|
|
76
|
+
} else {
|
|
77
|
+
setShowBrowserHint(true);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const showDismiss = !showIOSHint && !showBrowserHint;
|
|
82
|
+
const cornerMask = showDismiss
|
|
83
|
+
? 'radial-gradient(circle 20px at 100% 0, transparent 16px, #000 17px)'
|
|
84
|
+
: undefined;
|
|
85
|
+
|
|
86
|
+
const shellStyle: CSSProperties = {
|
|
87
|
+
position: 'fixed',
|
|
88
|
+
bottom: 16,
|
|
89
|
+
left: '50%',
|
|
90
|
+
zIndex: 50,
|
|
91
|
+
width: 'calc(100% - 2rem)',
|
|
92
|
+
maxWidth: 384,
|
|
93
|
+
transform: 'translateX(-50%)',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const cardStyle: CSSProperties = {
|
|
97
|
+
borderRadius: 24,
|
|
98
|
+
background: '#171718',
|
|
99
|
+
border: '0.6px solid #46454866',
|
|
100
|
+
boxShadow: '0px 8px 12px 6px #00000026',
|
|
101
|
+
padding: '12px 20px',
|
|
102
|
+
color: '#9D9DA2',
|
|
103
|
+
fontFamily: '"Google Sans", system-ui, -apple-system, sans-serif',
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
lineHeight: 1.4,
|
|
106
|
+
...(cornerMask
|
|
107
|
+
? { maskImage: cornerMask, WebkitMaskImage: cornerMask }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const installBtnStyle: CSSProperties = {
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
borderRadius: 8,
|
|
114
|
+
background: '#fafafa',
|
|
115
|
+
color: '#171718',
|
|
116
|
+
border: 'none',
|
|
117
|
+
padding: '6px 12px',
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
fontWeight: 500,
|
|
120
|
+
fontFamily: 'inherit',
|
|
121
|
+
flexShrink: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const dismissBtnStyle: CSSProperties = {
|
|
125
|
+
position: 'absolute',
|
|
126
|
+
top: -6,
|
|
127
|
+
right: -6,
|
|
128
|
+
zIndex: 10,
|
|
129
|
+
display: 'flex',
|
|
130
|
+
alignItems: 'center',
|
|
131
|
+
justifyContent: 'center',
|
|
132
|
+
width: 20,
|
|
133
|
+
height: 20,
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
color: '#737373',
|
|
137
|
+
background: '#171718',
|
|
138
|
+
border: '0.6px solid #46454866',
|
|
139
|
+
borderRadius: '9999px',
|
|
140
|
+
padding: 0,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div style={shellStyle}>
|
|
145
|
+
<div style={cardStyle}>
|
|
146
|
+
{showIOSHint ? (
|
|
147
|
+
<p style={{ margin: 0 }}>
|
|
148
|
+
To install: tap the "Share" button in Safari, then choose "Add to
|
|
149
|
+
Home Screen".
|
|
150
|
+
</p>
|
|
151
|
+
) : showBrowserHint ? (
|
|
152
|
+
<p style={{ margin: 0 }}>
|
|
153
|
+
To install: open your browser menu (⋮) and choose "Install app" or
|
|
154
|
+
"Add to Home Screen"
|
|
155
|
+
</p>
|
|
156
|
+
) : (
|
|
157
|
+
<div
|
|
158
|
+
style={{
|
|
159
|
+
display: 'flex',
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
justifyContent: 'space-between',
|
|
162
|
+
gap: 12,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<p style={{ margin: 0 }}>Install this app on your device</p>
|
|
166
|
+
<button type="button" onClick={install} style={installBtnStyle}>
|
|
167
|
+
Install
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
{showDismiss && (
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
onClick={dismiss}
|
|
176
|
+
aria-label="Dismiss install prompt"
|
|
177
|
+
style={dismissBtnStyle}
|
|
178
|
+
>
|
|
179
|
+
✕
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ManifestBranding = {
|
|
2
|
+
appName: string;
|
|
3
|
+
themeColor: string;
|
|
4
|
+
backgroundColor: string;
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
declare const MANIFEST_MARKER = "@mayson-pwa-manifest";
|
|
8
|
+
/** Next.js app/manifest.ts source with Mayson ownership marker. */
|
|
9
|
+
export declare function buildManifestSource(branding: ManifestBranding): string;
|
|
10
|
+
export { MANIFEST_MARKER };
|
|
11
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/manifest.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,QAAA,MAAM,eAAe,yBAAyB,CAAC;AAE/C,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAyCtE;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const MANIFEST_MARKER = '@mayson-pwa-manifest';
|
|
2
|
+
/** Next.js app/manifest.ts source with Mayson ownership marker. */
|
|
3
|
+
export function buildManifestSource(branding) {
|
|
4
|
+
const name = JSON.stringify(branding.appName);
|
|
5
|
+
const description = JSON.stringify(branding.description);
|
|
6
|
+
const theme = JSON.stringify(branding.themeColor);
|
|
7
|
+
const bg = JSON.stringify(branding.backgroundColor);
|
|
8
|
+
return `import type { MetadataRoute } from 'next';
|
|
9
|
+
|
|
10
|
+
// ${MANIFEST_MARKER}
|
|
11
|
+
// PWA manifest — served at /manifest.webmanifest.
|
|
12
|
+
export default function manifest(): MetadataRoute.Manifest {
|
|
13
|
+
return {
|
|
14
|
+
name: ${name},
|
|
15
|
+
short_name: ${name},
|
|
16
|
+
description: ${description},
|
|
17
|
+
start_url: '/',
|
|
18
|
+
scope: '/',
|
|
19
|
+
display: 'standalone',
|
|
20
|
+
background_color: ${bg},
|
|
21
|
+
theme_color: ${theme},
|
|
22
|
+
icons: [
|
|
23
|
+
{
|
|
24
|
+
src: '/icons/mayson-logo-dark-192x192.png',
|
|
25
|
+
sizes: '192x192',
|
|
26
|
+
type: 'image/png',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
src: '/icons/mayson-logo-dark-512x512.png',
|
|
30
|
+
sizes: '512x512',
|
|
31
|
+
type: 'image/png',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
src: '/icons/mayson-logo-dark-512x512-maskable.png',
|
|
35
|
+
sizes: '512x512',
|
|
36
|
+
type: 'image/png',
|
|
37
|
+
purpose: 'maskable',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export { MANIFEST_MARKER };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-worker-register.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/service-worker-register.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,wBAAgB,sCAAsC,IAAI,MAAM,CAqB/D"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Source for MaysonServiceWorkerRegister — production-only SW registration. */
|
|
2
|
+
export function buildMaysonServiceWorkerRegisterSource() {
|
|
3
|
+
return `'use client';
|
|
4
|
+
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
/** Registers the PWA service worker. Production-only — avoids caching stale HMR chunks in dev. */
|
|
8
|
+
export function MaysonServiceWorkerRegister() {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (process.env.NODE_ENV !== 'production') return;
|
|
11
|
+
if (!('serviceWorker' in navigator)) return;
|
|
12
|
+
const onLoad = () => {
|
|
13
|
+
navigator.serviceWorker.register('/sw.js').catch(() => {
|
|
14
|
+
// Installability is a nice-to-have — never break the app over it.
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
window.addEventListener('load', onLoad);
|
|
18
|
+
return () => window.removeEventListener('load', onLoad);
|
|
19
|
+
}, []);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const SW_MARKER = "@mayson-pwa-sw";
|
|
2
|
+
declare const CACHE_VERSION = "mayson-pwa-v1";
|
|
3
|
+
/** public/sw.js source with Mayson ownership marker. */
|
|
4
|
+
export declare function buildSwJsSource(): string;
|
|
5
|
+
export { SW_MARKER, CACHE_VERSION };
|
|
6
|
+
//# sourceMappingURL=sw.js.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sw.js.d.ts","sourceRoot":"","sources":["../../../../src/plugins/pwa/templates/sw.js.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,SAAS,mBAAmB,CAAC;AACnC,QAAA,MAAM,aAAa,kBAAkB,CAAC;AAEtC,wDAAwD;AACxD,wBAAgB,eAAe,IAAI,MAAM,CAkDxC;AAED,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC"}
|