@universal-pwa/core 0.1.0
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/LICENSE +22 -0
- package/README.md +151 -0
- package/dist/index.cjs +1368 -0
- package/dist/index.d.cts +348 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +1301 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1301 @@
|
|
|
1
|
+
// src/scanner/index.ts
|
|
2
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/scanner/framework-detector.ts
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function detectFramework(projectPath) {
|
|
8
|
+
const indicators = [];
|
|
9
|
+
let framework = null;
|
|
10
|
+
let confidence = "low";
|
|
11
|
+
if (existsSync(join(projectPath, "wp-config.php"))) {
|
|
12
|
+
indicators.push("wp-config.php");
|
|
13
|
+
if (existsSync(join(projectPath, "wp-content"))) {
|
|
14
|
+
indicators.push("wp-content/");
|
|
15
|
+
framework = "wordpress";
|
|
16
|
+
confidence = "high";
|
|
17
|
+
return { framework, confidence, indicators };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const composerPath = join(projectPath, "composer.json");
|
|
21
|
+
if (existsSync(composerPath)) {
|
|
22
|
+
try {
|
|
23
|
+
const composerContent = JSON.parse(readFileSync(composerPath, "utf-8"));
|
|
24
|
+
const dependencies = {
|
|
25
|
+
...composerContent.require ?? {},
|
|
26
|
+
...composerContent["require-dev"] ?? {}
|
|
27
|
+
};
|
|
28
|
+
if (dependencies["symfony/symfony"] || dependencies["symfony/framework-bundle"]) {
|
|
29
|
+
indicators.push("composer.json: symfony/*");
|
|
30
|
+
if (existsSync(join(projectPath, "public"))) {
|
|
31
|
+
indicators.push("public/");
|
|
32
|
+
framework = "symfony";
|
|
33
|
+
confidence = "high";
|
|
34
|
+
return { framework, confidence, indicators };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (dependencies["laravel/framework"]) {
|
|
38
|
+
indicators.push("composer.json: laravel/framework");
|
|
39
|
+
if (existsSync(join(projectPath, "public"))) {
|
|
40
|
+
indicators.push("public/");
|
|
41
|
+
framework = "laravel";
|
|
42
|
+
confidence = "high";
|
|
43
|
+
return { framework, confidence, indicators };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
50
|
+
if (existsSync(packageJsonPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const packageContent = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
53
|
+
const dependencies = {
|
|
54
|
+
...packageContent.dependencies ?? {},
|
|
55
|
+
...packageContent.devDependencies ?? {}
|
|
56
|
+
};
|
|
57
|
+
if (dependencies.next) {
|
|
58
|
+
indicators.push("package.json: next");
|
|
59
|
+
if (existsSync(join(projectPath, ".next"))) {
|
|
60
|
+
indicators.push(".next/");
|
|
61
|
+
framework = "nextjs";
|
|
62
|
+
confidence = "high";
|
|
63
|
+
return { framework, confidence, indicators };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (dependencies.nuxt) {
|
|
67
|
+
indicators.push("package.json: nuxt");
|
|
68
|
+
if (existsSync(join(projectPath, ".nuxt"))) {
|
|
69
|
+
indicators.push(".nuxt/");
|
|
70
|
+
framework = "nuxt";
|
|
71
|
+
confidence = "high";
|
|
72
|
+
return { framework, confidence, indicators };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (dependencies.react) {
|
|
76
|
+
indicators.push("package.json: react");
|
|
77
|
+
framework = "react";
|
|
78
|
+
confidence = framework ? "high" : "medium";
|
|
79
|
+
}
|
|
80
|
+
if (dependencies.vue) {
|
|
81
|
+
indicators.push("package.json: vue");
|
|
82
|
+
if (!framework) {
|
|
83
|
+
framework = "vue";
|
|
84
|
+
confidence = "high";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (dependencies["@angular/core"]) {
|
|
88
|
+
indicators.push("package.json: @angular/core");
|
|
89
|
+
if (!framework) {
|
|
90
|
+
framework = "angular";
|
|
91
|
+
confidence = "high";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!framework) {
|
|
98
|
+
const htmlFiles = ["index.html", "index.htm"];
|
|
99
|
+
const hasHtml = htmlFiles.some((file) => existsSync(join(projectPath, file)));
|
|
100
|
+
if (hasHtml) {
|
|
101
|
+
indicators.push("HTML files present");
|
|
102
|
+
framework = "static";
|
|
103
|
+
confidence = "medium";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { framework, confidence, indicators };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/scanner/asset-detector.ts
|
|
110
|
+
import { glob } from "glob";
|
|
111
|
+
import { join as join2 } from "path";
|
|
112
|
+
import { statSync } from "fs";
|
|
113
|
+
var IGNORED_PATTERNS = [
|
|
114
|
+
"**/node_modules/**",
|
|
115
|
+
"**/.git/**",
|
|
116
|
+
"**/dist/**",
|
|
117
|
+
"**/.next/**",
|
|
118
|
+
"**/.nuxt/**",
|
|
119
|
+
"**/build/**",
|
|
120
|
+
"**/coverage/**"
|
|
121
|
+
];
|
|
122
|
+
var JS_EXTENSIONS = [".js", ".mjs", ".ts", ".tsx", ".jsx"];
|
|
123
|
+
var CSS_EXTENSIONS = [".css", ".scss", ".sass", ".less"];
|
|
124
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".ico"];
|
|
125
|
+
var FONT_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf", ".eot"];
|
|
126
|
+
var API_PATTERNS = [
|
|
127
|
+
"/api/**",
|
|
128
|
+
"/graphql",
|
|
129
|
+
"/rest/**",
|
|
130
|
+
"/v1/**",
|
|
131
|
+
"/v2/**"
|
|
132
|
+
];
|
|
133
|
+
async function detectAssets(projectPath) {
|
|
134
|
+
const result = {
|
|
135
|
+
javascript: [],
|
|
136
|
+
css: [],
|
|
137
|
+
images: [],
|
|
138
|
+
fonts: [],
|
|
139
|
+
apiRoutes: []
|
|
140
|
+
};
|
|
141
|
+
const jsExtensionsStr = JS_EXTENSIONS.map((ext) => ext.slice(1)).join(",");
|
|
142
|
+
const jsPattern = `**/*.{${jsExtensionsStr}}`;
|
|
143
|
+
const jsRootPattern = `*.{${jsExtensionsStr}}`;
|
|
144
|
+
const [jsFiles, jsRootFiles] = await Promise.all([
|
|
145
|
+
glob(jsPattern, {
|
|
146
|
+
cwd: projectPath,
|
|
147
|
+
ignore: IGNORED_PATTERNS,
|
|
148
|
+
absolute: true,
|
|
149
|
+
nodir: true
|
|
150
|
+
}),
|
|
151
|
+
glob(jsRootPattern, {
|
|
152
|
+
cwd: projectPath,
|
|
153
|
+
ignore: IGNORED_PATTERNS,
|
|
154
|
+
absolute: true,
|
|
155
|
+
nodir: true
|
|
156
|
+
})
|
|
157
|
+
]);
|
|
158
|
+
const allJsFiles = /* @__PURE__ */ new Set([...jsFiles, ...jsRootFiles]);
|
|
159
|
+
result.javascript.push(...allJsFiles);
|
|
160
|
+
const cssExtensionsStr = CSS_EXTENSIONS.map((ext) => ext.slice(1)).join(",");
|
|
161
|
+
const cssPattern = `**/*.{${cssExtensionsStr}}`;
|
|
162
|
+
const cssFiles = await glob(cssPattern, {
|
|
163
|
+
cwd: projectPath,
|
|
164
|
+
ignore: IGNORED_PATTERNS,
|
|
165
|
+
absolute: false,
|
|
166
|
+
nodir: true
|
|
167
|
+
});
|
|
168
|
+
const cssRootPattern = `*.{${cssExtensionsStr}}`;
|
|
169
|
+
const cssRootFiles = await glob(cssRootPattern, {
|
|
170
|
+
cwd: projectPath,
|
|
171
|
+
ignore: IGNORED_PATTERNS,
|
|
172
|
+
absolute: false,
|
|
173
|
+
nodir: true
|
|
174
|
+
});
|
|
175
|
+
const allCssFiles = [.../* @__PURE__ */ new Set([...cssFiles, ...cssRootFiles])];
|
|
176
|
+
result.css.push(...allCssFiles.map((f) => join2(projectPath, f)));
|
|
177
|
+
const imagePatterns = IMAGE_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
|
|
178
|
+
const allImageFiles = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const pattern of imagePatterns) {
|
|
180
|
+
const files = await glob(pattern, {
|
|
181
|
+
cwd: projectPath,
|
|
182
|
+
ignore: IGNORED_PATTERNS,
|
|
183
|
+
absolute: false,
|
|
184
|
+
nodir: true
|
|
185
|
+
});
|
|
186
|
+
files.forEach((f) => allImageFiles.add(f));
|
|
187
|
+
}
|
|
188
|
+
result.images.push(...Array.from(allImageFiles).map((f) => join2(projectPath, f)));
|
|
189
|
+
const fontPatterns = FONT_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
|
|
190
|
+
const allFontFiles = /* @__PURE__ */ new Set();
|
|
191
|
+
for (const pattern of fontPatterns) {
|
|
192
|
+
const files = await glob(pattern, {
|
|
193
|
+
cwd: projectPath,
|
|
194
|
+
ignore: IGNORED_PATTERNS,
|
|
195
|
+
absolute: false,
|
|
196
|
+
nodir: true
|
|
197
|
+
});
|
|
198
|
+
files.forEach((f) => allFontFiles.add(f));
|
|
199
|
+
}
|
|
200
|
+
result.fonts.push(...Array.from(allFontFiles).map((f) => join2(projectPath, f)));
|
|
201
|
+
const routeFiles = await glob(`**/*{route,api,graphql}*.{js,ts,json}`, {
|
|
202
|
+
cwd: projectPath,
|
|
203
|
+
ignore: IGNORED_PATTERNS,
|
|
204
|
+
absolute: false,
|
|
205
|
+
nodir: true
|
|
206
|
+
});
|
|
207
|
+
if (routeFiles.length > 0) {
|
|
208
|
+
result.apiRoutes.push(...API_PATTERNS);
|
|
209
|
+
}
|
|
210
|
+
result.javascript = result.javascript.filter((f) => {
|
|
211
|
+
try {
|
|
212
|
+
return statSync(f).isFile();
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
result.css = result.css.filter((f) => {
|
|
218
|
+
try {
|
|
219
|
+
return statSync(f).isFile();
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
result.images = result.images.filter((f) => {
|
|
225
|
+
try {
|
|
226
|
+
return statSync(f).isFile();
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
result.fonts = result.fonts.filter((f) => {
|
|
232
|
+
try {
|
|
233
|
+
return statSync(f).isFile();
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/scanner/architecture-detector.ts
|
|
242
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
243
|
+
import { join as join3 } from "path";
|
|
244
|
+
import { glob as glob2 } from "glob";
|
|
245
|
+
async function detectArchitecture(projectPath) {
|
|
246
|
+
const indicators = [];
|
|
247
|
+
let architecture = "static";
|
|
248
|
+
let buildTool = null;
|
|
249
|
+
let confidence = "low";
|
|
250
|
+
const packageJsonPath = join3(projectPath, "package.json");
|
|
251
|
+
if (existsSync2(packageJsonPath)) {
|
|
252
|
+
try {
|
|
253
|
+
const packageContent = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
254
|
+
const dependencies = {
|
|
255
|
+
...packageContent.dependencies ?? {},
|
|
256
|
+
...packageContent.devDependencies ?? {}
|
|
257
|
+
};
|
|
258
|
+
if (dependencies.vite || packageContent.devDependencies?.["vite"]) {
|
|
259
|
+
indicators.push("package.json: vite");
|
|
260
|
+
buildTool = "vite";
|
|
261
|
+
confidence = "high";
|
|
262
|
+
} else if (dependencies.webpack || packageContent.devDependencies?.["webpack"]) {
|
|
263
|
+
indicators.push("package.json: webpack");
|
|
264
|
+
buildTool = "webpack";
|
|
265
|
+
confidence = "high";
|
|
266
|
+
} else if (dependencies.rollup || packageContent.devDependencies?.["rollup"]) {
|
|
267
|
+
indicators.push("package.json: rollup");
|
|
268
|
+
buildTool = "rollup";
|
|
269
|
+
confidence = "high";
|
|
270
|
+
} else if (dependencies.esbuild || packageContent.devDependencies?.["esbuild"]) {
|
|
271
|
+
indicators.push("package.json: esbuild");
|
|
272
|
+
buildTool = "esbuild";
|
|
273
|
+
confidence = "high";
|
|
274
|
+
} else if (dependencies.parcel || packageContent.devDependencies?.["parcel"]) {
|
|
275
|
+
indicators.push("package.json: parcel");
|
|
276
|
+
buildTool = "parcel";
|
|
277
|
+
confidence = "high";
|
|
278
|
+
} else if (dependencies["@turbo/gen"] || packageContent.devDependencies?.["@turbo/gen"]) {
|
|
279
|
+
indicators.push("package.json: turbopack");
|
|
280
|
+
buildTool = "turbopack";
|
|
281
|
+
confidence = "high";
|
|
282
|
+
}
|
|
283
|
+
if (dependencies.next || packageContent.dependencies?.["next"] || packageContent.devDependencies?.["next"]) {
|
|
284
|
+
architecture = "ssr";
|
|
285
|
+
confidence = "high";
|
|
286
|
+
indicators.push("Next.js detected \u2192 SSR");
|
|
287
|
+
} else if (dependencies.nuxt || packageContent.dependencies?.["nuxt"] || packageContent.devDependencies?.["nuxt"]) {
|
|
288
|
+
architecture = "ssr";
|
|
289
|
+
confidence = "high";
|
|
290
|
+
indicators.push("Nuxt detected \u2192 SSR");
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const htmlPatterns = ["**/*.html", "*.html", "index.html"];
|
|
296
|
+
const allHtmlFiles = /* @__PURE__ */ new Set();
|
|
297
|
+
for (const pattern of htmlPatterns) {
|
|
298
|
+
const files = await glob2(pattern, {
|
|
299
|
+
cwd: projectPath,
|
|
300
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**"],
|
|
301
|
+
absolute: false,
|
|
302
|
+
nodir: true
|
|
303
|
+
});
|
|
304
|
+
files.forEach((f) => allHtmlFiles.add(f));
|
|
305
|
+
}
|
|
306
|
+
if (allHtmlFiles.size > 0 && architecture !== "ssr") {
|
|
307
|
+
const htmlArray = Array.from(allHtmlFiles);
|
|
308
|
+
const firstHtml = htmlArray.find((f) => f === "index.html" || f.endsWith("/index.html")) || htmlArray[0];
|
|
309
|
+
const htmlPath = join3(projectPath, firstHtml);
|
|
310
|
+
try {
|
|
311
|
+
const htmlContent = readFileSync2(htmlPath, "utf-8").toLowerCase();
|
|
312
|
+
const spaPatterns = [
|
|
313
|
+
/<div[^>]*id=["']root["']/i,
|
|
314
|
+
/<div[^>]*id=["']app["']/i,
|
|
315
|
+
/<div[^>]*id=["']main["']/i,
|
|
316
|
+
/react-dom/i,
|
|
317
|
+
/mount\(/i,
|
|
318
|
+
/createRoot\(/i
|
|
319
|
+
];
|
|
320
|
+
const ssrPatterns = [
|
|
321
|
+
/<body[^>]*>[\s\S]{100,}/i,
|
|
322
|
+
// Body avec beaucoup de contenu
|
|
323
|
+
/<article/i,
|
|
324
|
+
/<main[^>]*>[\s\S]{50,}/i,
|
|
325
|
+
/hydrat/i,
|
|
326
|
+
/__next/i,
|
|
327
|
+
/__next_data__/i,
|
|
328
|
+
/nuxt/i
|
|
329
|
+
];
|
|
330
|
+
const hasSpaPattern = spaPatterns.some((pattern) => pattern.test(htmlContent));
|
|
331
|
+
const hasSsrPattern = ssrPatterns.some((pattern) => pattern.test(htmlContent));
|
|
332
|
+
const isLargeContent = htmlContent.length > 2e3;
|
|
333
|
+
const hasRealContent = htmlContent.replace(/<[^>]+>/g, "").trim().length > 100;
|
|
334
|
+
if (isLargeContent && hasRealContent && !hasSsrPattern) {
|
|
335
|
+
indicators.push("HTML: very large content with real text \u2192 SSR");
|
|
336
|
+
if (hasSpaPattern) {
|
|
337
|
+
indicators.push("HTML: SPA pattern but large content \u2192 SSR");
|
|
338
|
+
}
|
|
339
|
+
architecture = "ssr";
|
|
340
|
+
confidence = "high";
|
|
341
|
+
} else if (hasSsrPattern && htmlContent.length > 1e3) {
|
|
342
|
+
indicators.push("HTML: SSR patterns detected (large content)");
|
|
343
|
+
if (isLargeContent && hasRealContent) {
|
|
344
|
+
indicators.push("HTML: very large content with real text \u2192 SSR");
|
|
345
|
+
}
|
|
346
|
+
if (hasSpaPattern) {
|
|
347
|
+
indicators.push("HTML: SPA pattern but large content \u2192 SSR");
|
|
348
|
+
}
|
|
349
|
+
architecture = "ssr";
|
|
350
|
+
confidence = "high";
|
|
351
|
+
} else if (hasSsrPattern && !hasSpaPattern) {
|
|
352
|
+
indicators.push("HTML: SSR patterns detected");
|
|
353
|
+
if (htmlContent.length > 500) {
|
|
354
|
+
indicators.push("HTML: large content");
|
|
355
|
+
}
|
|
356
|
+
architecture = "ssr";
|
|
357
|
+
confidence = "high";
|
|
358
|
+
} else if (hasSpaPattern) {
|
|
359
|
+
if (isLargeContent && hasRealContent) {
|
|
360
|
+
indicators.push("HTML: SPA pattern but large content \u2192 SSR");
|
|
361
|
+
architecture = "ssr";
|
|
362
|
+
confidence = "medium";
|
|
363
|
+
} else {
|
|
364
|
+
indicators.push("HTML: SPA patterns detected");
|
|
365
|
+
architecture = "spa";
|
|
366
|
+
confidence = "high";
|
|
367
|
+
}
|
|
368
|
+
} else if (htmlContent.length > 500) {
|
|
369
|
+
indicators.push("HTML: large content");
|
|
370
|
+
architecture = "ssr";
|
|
371
|
+
confidence = "medium";
|
|
372
|
+
} else {
|
|
373
|
+
indicators.push("HTML: minimal content");
|
|
374
|
+
architecture = "static";
|
|
375
|
+
confidence = "medium";
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const jsFiles = await glob2("**/*.{js,ts,tsx,jsx}", {
|
|
381
|
+
cwd: projectPath,
|
|
382
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**", "**/*.test.*", "**/*.spec.*"],
|
|
383
|
+
absolute: false,
|
|
384
|
+
nodir: true
|
|
385
|
+
});
|
|
386
|
+
if (jsFiles.length > 0 && architecture === "static") {
|
|
387
|
+
let routerFilesFound = 0;
|
|
388
|
+
for (const jsFile of jsFiles.slice(0, 10)) {
|
|
389
|
+
try {
|
|
390
|
+
const jsPath = join3(projectPath, jsFile);
|
|
391
|
+
const jsContent = readFileSync2(jsPath, "utf-8").toLowerCase();
|
|
392
|
+
const routerPatterns = [
|
|
393
|
+
/react-router/i,
|
|
394
|
+
/vue-router/i,
|
|
395
|
+
/@angular\/router/i,
|
|
396
|
+
/next\/router/i,
|
|
397
|
+
/nuxt/i,
|
|
398
|
+
/createBrowserRouter/i,
|
|
399
|
+
/BrowserRouter/i,
|
|
400
|
+
/Router/i
|
|
401
|
+
];
|
|
402
|
+
if (routerPatterns.some((pattern) => pattern.test(jsContent))) {
|
|
403
|
+
routerFilesFound++;
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (routerFilesFound > 0) {
|
|
409
|
+
indicators.push(`JS: router patterns found (${routerFilesFound} files)`);
|
|
410
|
+
if (architecture === "static") {
|
|
411
|
+
architecture = "spa";
|
|
412
|
+
confidence = confidence === "low" ? "medium" : confidence;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { architecture, buildTool, confidence, indicators };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/scanner/index.ts
|
|
420
|
+
async function scanProject(options) {
|
|
421
|
+
const { projectPath, includeAssets = true, includeArchitecture = true } = options;
|
|
422
|
+
const frameworkCandidate = detectFramework(projectPath);
|
|
423
|
+
const framework = isFrameworkDetectionResult(frameworkCandidate) ? frameworkCandidate : { framework: null, confidence: "low", indicators: [] };
|
|
424
|
+
const assetsCandidate = includeAssets ? await detectAssets(projectPath) : getEmptyAssets();
|
|
425
|
+
const assets = isAssetDetectionResult(assetsCandidate) ? assetsCandidate : getEmptyAssets();
|
|
426
|
+
const architectureCandidate = includeArchitecture ? await detectArchitecture(projectPath) : getEmptyArchitecture();
|
|
427
|
+
const architecture = isArchitectureDetectionResult(architectureCandidate) ? architectureCandidate : getEmptyArchitecture();
|
|
428
|
+
return {
|
|
429
|
+
framework,
|
|
430
|
+
assets,
|
|
431
|
+
architecture,
|
|
432
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
433
|
+
projectPath
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function generateReport(result) {
|
|
437
|
+
return JSON.stringify(result, null, 2);
|
|
438
|
+
}
|
|
439
|
+
function validateProjectPath(projectPath) {
|
|
440
|
+
try {
|
|
441
|
+
return existsSync3(projectPath);
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isFrameworkDetectionResult(value) {
|
|
447
|
+
if (!value || typeof value !== "object") return false;
|
|
448
|
+
const v = value;
|
|
449
|
+
return (v.framework === null || typeof v.framework === "string") && (v.confidence === "low" || v.confidence === "medium" || v.confidence === "high") && Array.isArray(v.indicators);
|
|
450
|
+
}
|
|
451
|
+
function isAssetDetectionResult(value) {
|
|
452
|
+
if (!value || typeof value !== "object") return false;
|
|
453
|
+
const v = value;
|
|
454
|
+
const isStringArray = (x) => Array.isArray(x) && x.every((i) => typeof i === "string");
|
|
455
|
+
return isStringArray(v.javascript) && isStringArray(v.css) && isStringArray(v.images) && isStringArray(v.fonts) && isStringArray(v.apiRoutes);
|
|
456
|
+
}
|
|
457
|
+
function isArchitectureDetectionResult(value) {
|
|
458
|
+
if (!value || typeof value !== "object") return false;
|
|
459
|
+
const v = value;
|
|
460
|
+
const isArch = v.architecture === "spa" || v.architecture === "ssr" || v.architecture === "static";
|
|
461
|
+
const isBuildTool = v.buildTool === null || v.buildTool === "vite" || v.buildTool === "webpack" || v.buildTool === "rollup";
|
|
462
|
+
const isConfidence = v.confidence === "low" || v.confidence === "medium" || v.confidence === "high";
|
|
463
|
+
return isArch && isBuildTool && isConfidence && Array.isArray(v.indicators);
|
|
464
|
+
}
|
|
465
|
+
function getEmptyAssets() {
|
|
466
|
+
return {
|
|
467
|
+
javascript: [],
|
|
468
|
+
css: [],
|
|
469
|
+
images: [],
|
|
470
|
+
fonts: [],
|
|
471
|
+
apiRoutes: []
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function getEmptyArchitecture() {
|
|
475
|
+
return {
|
|
476
|
+
architecture: "static",
|
|
477
|
+
buildTool: null,
|
|
478
|
+
confidence: "low",
|
|
479
|
+
indicators: []
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/generator/manifest-generator.ts
|
|
484
|
+
import { z } from "zod";
|
|
485
|
+
import { writeFileSync } from "fs";
|
|
486
|
+
import { join as join4 } from "path";
|
|
487
|
+
var ManifestIconSchema = z.object({
|
|
488
|
+
src: z.string(),
|
|
489
|
+
sizes: z.string(),
|
|
490
|
+
type: z.string().optional(),
|
|
491
|
+
purpose: z.string().optional()
|
|
492
|
+
});
|
|
493
|
+
var ManifestSplashScreenSchema = z.object({
|
|
494
|
+
src: z.string(),
|
|
495
|
+
sizes: z.string(),
|
|
496
|
+
type: z.string().optional()
|
|
497
|
+
});
|
|
498
|
+
var ManifestSchema = z.object({
|
|
499
|
+
name: z.string().min(1),
|
|
500
|
+
short_name: z.string().min(1).max(12),
|
|
501
|
+
description: z.string().optional(),
|
|
502
|
+
start_url: z.string().default("/"),
|
|
503
|
+
scope: z.string().default("/"),
|
|
504
|
+
display: z.enum(["standalone", "fullscreen", "minimal-ui", "browser"]).default("standalone"),
|
|
505
|
+
orientation: z.enum(["any", "portrait", "landscape"]).optional(),
|
|
506
|
+
theme_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
|
507
|
+
background_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
|
508
|
+
icons: z.array(ManifestIconSchema).min(1),
|
|
509
|
+
splash_screens: z.array(ManifestSplashScreenSchema).optional(),
|
|
510
|
+
categories: z.array(z.string()).optional(),
|
|
511
|
+
lang: z.string().optional(),
|
|
512
|
+
dir: z.enum(["ltr", "rtl", "auto"]).optional(),
|
|
513
|
+
prefer_related_applications: z.boolean().optional(),
|
|
514
|
+
related_applications: z.array(z.unknown()).optional()
|
|
515
|
+
});
|
|
516
|
+
function generateManifest(options) {
|
|
517
|
+
const manifest = {
|
|
518
|
+
name: options.name,
|
|
519
|
+
short_name: options.shortName,
|
|
520
|
+
start_url: options.startUrl ?? "/",
|
|
521
|
+
scope: options.scope ?? "/",
|
|
522
|
+
display: options.display ?? "standalone",
|
|
523
|
+
icons: options.icons
|
|
524
|
+
};
|
|
525
|
+
if (options.description) {
|
|
526
|
+
manifest.description = options.description;
|
|
527
|
+
}
|
|
528
|
+
if (options.orientation) {
|
|
529
|
+
manifest.orientation = options.orientation;
|
|
530
|
+
}
|
|
531
|
+
if (options.themeColor) {
|
|
532
|
+
manifest.theme_color = options.themeColor;
|
|
533
|
+
}
|
|
534
|
+
if (options.backgroundColor) {
|
|
535
|
+
manifest.background_color = options.backgroundColor;
|
|
536
|
+
}
|
|
537
|
+
if (options.splashScreens && options.splashScreens.length > 0) {
|
|
538
|
+
manifest.splash_screens = options.splashScreens;
|
|
539
|
+
}
|
|
540
|
+
if (options.categories && options.categories.length > 0) {
|
|
541
|
+
manifest.categories = options.categories;
|
|
542
|
+
}
|
|
543
|
+
if (options.lang) {
|
|
544
|
+
manifest.lang = options.lang;
|
|
545
|
+
}
|
|
546
|
+
if (options.dir) {
|
|
547
|
+
manifest.dir = options.dir;
|
|
548
|
+
}
|
|
549
|
+
if (options.preferRelatedApplications !== void 0) {
|
|
550
|
+
manifest.prefer_related_applications = options.preferRelatedApplications;
|
|
551
|
+
}
|
|
552
|
+
if (options.relatedApplications && options.relatedApplications.length > 0) {
|
|
553
|
+
manifest.related_applications = options.relatedApplications;
|
|
554
|
+
}
|
|
555
|
+
return ManifestSchema.parse(manifest);
|
|
556
|
+
}
|
|
557
|
+
function writeManifest(manifest, outputDir) {
|
|
558
|
+
const manifestPath = join4(outputDir, "manifest.json");
|
|
559
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
560
|
+
writeFileSync(manifestPath, manifestJson, "utf-8");
|
|
561
|
+
return manifestPath;
|
|
562
|
+
}
|
|
563
|
+
function generateAndWriteManifest(options, outputDir) {
|
|
564
|
+
const manifest = generateManifest(options);
|
|
565
|
+
return writeManifest(manifest, outputDir);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/generator/icon-generator.ts
|
|
569
|
+
import sharp from "sharp";
|
|
570
|
+
import { existsSync as existsSync4, mkdirSync } from "fs";
|
|
571
|
+
import { join as join5 } from "path";
|
|
572
|
+
var STANDARD_ICON_SIZES = [
|
|
573
|
+
{ width: 72, height: 72, name: "icon-72x72.png" },
|
|
574
|
+
{ width: 96, height: 96, name: "icon-96x96.png" },
|
|
575
|
+
{ width: 128, height: 128, name: "icon-128x128.png" },
|
|
576
|
+
{ width: 144, height: 144, name: "icon-144x144.png" },
|
|
577
|
+
{ width: 152, height: 152, name: "icon-152x152.png" },
|
|
578
|
+
{ width: 192, height: 192, name: "icon-192x192.png" },
|
|
579
|
+
{ width: 384, height: 384, name: "icon-384x384.png" },
|
|
580
|
+
{ width: 512, height: 512, name: "icon-512x512.png" }
|
|
581
|
+
];
|
|
582
|
+
var STANDARD_SPLASH_SIZES = [
|
|
583
|
+
{ width: 640, height: 1136, name: "splash-640x1136.png" },
|
|
584
|
+
// iPhone 5
|
|
585
|
+
{ width: 750, height: 1334, name: "splash-750x1334.png" },
|
|
586
|
+
// iPhone 6/7/8
|
|
587
|
+
{ width: 828, height: 1792, name: "splash-828x1792.png" },
|
|
588
|
+
// iPhone XR
|
|
589
|
+
{ width: 1125, height: 2436, name: "splash-1125x2436.png" },
|
|
590
|
+
// iPhone X/XS
|
|
591
|
+
{ width: 1242, height: 2688, name: "splash-1242x2688.png" },
|
|
592
|
+
// iPhone XS Max
|
|
593
|
+
{ width: 1536, height: 2048, name: "splash-1536x2048.png" },
|
|
594
|
+
// iPad
|
|
595
|
+
{ width: 2048, height: 2732, name: "splash-2048x2732.png" }
|
|
596
|
+
// iPad Pro
|
|
597
|
+
];
|
|
598
|
+
async function generateIcons(options) {
|
|
599
|
+
const {
|
|
600
|
+
sourceImage,
|
|
601
|
+
outputDir,
|
|
602
|
+
iconSizes = STANDARD_ICON_SIZES,
|
|
603
|
+
splashSizes = STANDARD_SPLASH_SIZES,
|
|
604
|
+
format = "png",
|
|
605
|
+
quality = 90
|
|
606
|
+
} = options;
|
|
607
|
+
if (!existsSync4(sourceImage)) {
|
|
608
|
+
throw new Error(`Source image not found: ${sourceImage}`);
|
|
609
|
+
}
|
|
610
|
+
mkdirSync(outputDir, { recursive: true });
|
|
611
|
+
const generatedFiles = [];
|
|
612
|
+
const icons = [];
|
|
613
|
+
const splashScreens = [];
|
|
614
|
+
const image = sharp(sourceImage);
|
|
615
|
+
const metadata = await image.metadata();
|
|
616
|
+
if (!metadata.width || !metadata.height) {
|
|
617
|
+
throw new Error("Unable to read image dimensions");
|
|
618
|
+
}
|
|
619
|
+
for (const size of iconSizes) {
|
|
620
|
+
const outputPath = join5(outputDir, size.name);
|
|
621
|
+
try {
|
|
622
|
+
let pipeline = image.clone().resize(size.width, size.height, {
|
|
623
|
+
fit: "cover",
|
|
624
|
+
position: "center"
|
|
625
|
+
});
|
|
626
|
+
if (format === "png") {
|
|
627
|
+
pipeline = pipeline.png({ quality, compressionLevel: 9 });
|
|
628
|
+
} else {
|
|
629
|
+
pipeline = pipeline.webp({ quality });
|
|
630
|
+
}
|
|
631
|
+
await pipeline.toFile(outputPath);
|
|
632
|
+
generatedFiles.push(outputPath);
|
|
633
|
+
icons.push({
|
|
634
|
+
src: `/${size.name}`,
|
|
635
|
+
sizes: `${size.width}x${size.height}`,
|
|
636
|
+
type: format === "png" ? "image/png" : "image/webp",
|
|
637
|
+
purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
|
|
638
|
+
});
|
|
639
|
+
} catch (err) {
|
|
640
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
641
|
+
throw new Error(`Failed to generate icon ${size.name}: ${message}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const size of splashSizes) {
|
|
645
|
+
const outputPath = join5(outputDir, size.name);
|
|
646
|
+
try {
|
|
647
|
+
let pipeline = image.clone().resize(size.width, size.height, {
|
|
648
|
+
fit: "cover",
|
|
649
|
+
position: "center"
|
|
650
|
+
});
|
|
651
|
+
if (format === "png") {
|
|
652
|
+
pipeline = pipeline.png({ quality, compressionLevel: 9 });
|
|
653
|
+
} else {
|
|
654
|
+
pipeline = pipeline.webp({ quality });
|
|
655
|
+
}
|
|
656
|
+
await pipeline.toFile(outputPath);
|
|
657
|
+
generatedFiles.push(outputPath);
|
|
658
|
+
splashScreens.push({
|
|
659
|
+
src: `/${size.name}`,
|
|
660
|
+
sizes: `${size.width}x${size.height}`,
|
|
661
|
+
type: format === "png" ? "image/png" : "image/webp"
|
|
662
|
+
});
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
665
|
+
throw new Error(`Failed to generate splash screen ${size.name}: ${message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
icons,
|
|
670
|
+
splashScreens,
|
|
671
|
+
generatedFiles
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async function generateIconsOnly(options) {
|
|
675
|
+
const result = await generateIcons({
|
|
676
|
+
...options,
|
|
677
|
+
splashSizes: []
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
icons: result.icons,
|
|
681
|
+
generatedFiles: result.generatedFiles
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
async function generateSplashScreensOnly(options) {
|
|
685
|
+
const result = await generateIcons({
|
|
686
|
+
...options,
|
|
687
|
+
iconSizes: []
|
|
688
|
+
});
|
|
689
|
+
return {
|
|
690
|
+
splashScreens: result.splashScreens,
|
|
691
|
+
generatedFiles: result.generatedFiles
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async function generateFavicon(sourceImage, outputDir) {
|
|
695
|
+
if (!existsSync4(sourceImage)) {
|
|
696
|
+
throw new Error(`Source image not found: ${sourceImage}`);
|
|
697
|
+
}
|
|
698
|
+
mkdirSync(outputDir, { recursive: true });
|
|
699
|
+
const faviconPath = join5(outputDir, "favicon.ico");
|
|
700
|
+
try {
|
|
701
|
+
await sharp(sourceImage).resize(32, 32, {
|
|
702
|
+
fit: "cover",
|
|
703
|
+
position: "center"
|
|
704
|
+
}).png().toFile(faviconPath);
|
|
705
|
+
return faviconPath;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
708
|
+
throw new Error(`Failed to generate favicon: ${message}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function generateAppleTouchIcon(sourceImage, outputDir) {
|
|
712
|
+
if (!existsSync4(sourceImage)) {
|
|
713
|
+
throw new Error(`Source image not found: ${sourceImage}`);
|
|
714
|
+
}
|
|
715
|
+
mkdirSync(outputDir, { recursive: true });
|
|
716
|
+
const appleIconPath = join5(outputDir, "apple-touch-icon.png");
|
|
717
|
+
try {
|
|
718
|
+
await sharp(sourceImage).resize(180, 180, {
|
|
719
|
+
fit: "cover",
|
|
720
|
+
position: "center"
|
|
721
|
+
}).png({ quality: 90, compressionLevel: 9 }).toFile(appleIconPath);
|
|
722
|
+
return appleIconPath;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
725
|
+
throw new Error(`Failed to generate apple-touch-icon: ${message}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/generator/service-worker-generator.ts
|
|
730
|
+
import { injectManifest, generateSW } from "workbox-build";
|
|
731
|
+
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
|
|
732
|
+
import { join as join6 } from "path";
|
|
733
|
+
import { getServiceWorkerTemplate, determineTemplateType } from "@universal-pwa/templates";
|
|
734
|
+
async function generateServiceWorker(options) {
|
|
735
|
+
const {
|
|
736
|
+
projectPath,
|
|
737
|
+
outputDir,
|
|
738
|
+
architecture,
|
|
739
|
+
framework,
|
|
740
|
+
templateType,
|
|
741
|
+
globDirectory,
|
|
742
|
+
globPatterns = ["**/*.{js,css,html,png,jpg,jpeg,svg,webp,woff,woff2,ttf,otf}"],
|
|
743
|
+
swDest = "sw.js",
|
|
744
|
+
offlinePage
|
|
745
|
+
} = options;
|
|
746
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
747
|
+
const finalTemplateType = templateType ?? determineTemplateType(architecture, framework ?? null);
|
|
748
|
+
const template = getServiceWorkerTemplate(finalTemplateType);
|
|
749
|
+
const swSrcPath = join6(outputDir, "sw-src.js");
|
|
750
|
+
writeFileSync2(swSrcPath, template.content, "utf-8");
|
|
751
|
+
const swDestPath = join6(outputDir, swDest);
|
|
752
|
+
const workboxConfig = {
|
|
753
|
+
globDirectory: globDirectory ?? projectPath,
|
|
754
|
+
globPatterns,
|
|
755
|
+
swDest: swDestPath,
|
|
756
|
+
swSrc: swSrcPath,
|
|
757
|
+
// Injection du manifest dans le template
|
|
758
|
+
injectionPoint: "self.__WB_MANIFEST"
|
|
759
|
+
};
|
|
760
|
+
if (offlinePage) {
|
|
761
|
+
workboxConfig.globPatterns = [...workboxConfig.globPatterns ?? [], offlinePage];
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
const result = await injectManifest(workboxConfig);
|
|
765
|
+
try {
|
|
766
|
+
if (existsSync5(swSrcPath)) {
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
const resultFilePaths = result.filePaths ?? [];
|
|
771
|
+
const normalizedOffline = offlinePage ? offlinePage.replace(/^\.\//, "") : void 0;
|
|
772
|
+
const finalFilePaths = normalizedOffline && !resultFilePaths.some((p) => p.includes(normalizedOffline)) ? [...resultFilePaths, normalizedOffline] : resultFilePaths;
|
|
773
|
+
return {
|
|
774
|
+
swPath: swDestPath,
|
|
775
|
+
count: result.count,
|
|
776
|
+
size: result.size,
|
|
777
|
+
warnings: result.warnings ?? [],
|
|
778
|
+
filePaths: finalFilePaths
|
|
779
|
+
};
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
782
|
+
throw new Error(`Failed to generate service worker: ${message}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function generateSimpleServiceWorker(options) {
|
|
786
|
+
const {
|
|
787
|
+
projectPath,
|
|
788
|
+
outputDir,
|
|
789
|
+
globDirectory,
|
|
790
|
+
globPatterns = ["**/*.{js,css,html,png,jpg,jpeg,svg,webp,woff,woff2,ttf,otf}"],
|
|
791
|
+
swDest = "sw.js",
|
|
792
|
+
skipWaiting = true,
|
|
793
|
+
clientsClaim = true,
|
|
794
|
+
runtimeCaching
|
|
795
|
+
} = options;
|
|
796
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
797
|
+
const swDestPath = join6(outputDir, swDest);
|
|
798
|
+
const workboxConfig = {
|
|
799
|
+
globDirectory: globDirectory ?? projectPath,
|
|
800
|
+
globPatterns,
|
|
801
|
+
swDest: swDestPath,
|
|
802
|
+
skipWaiting,
|
|
803
|
+
clientsClaim,
|
|
804
|
+
mode: "production",
|
|
805
|
+
sourcemap: false
|
|
806
|
+
};
|
|
807
|
+
if (runtimeCaching && runtimeCaching.length > 0) {
|
|
808
|
+
workboxConfig.runtimeCaching = runtimeCaching.map((cache) => {
|
|
809
|
+
const handlerMap = {
|
|
810
|
+
NetworkFirst: "NetworkFirst",
|
|
811
|
+
CacheFirst: "CacheFirst",
|
|
812
|
+
StaleWhileRevalidate: "StaleWhileRevalidate",
|
|
813
|
+
NetworkOnly: "NetworkOnly",
|
|
814
|
+
CacheOnly: "CacheOnly"
|
|
815
|
+
};
|
|
816
|
+
const handler = handlerMap[cache.handler] ?? cache.handler;
|
|
817
|
+
return {
|
|
818
|
+
urlPattern: typeof cache.urlPattern === "string" ? new RegExp(cache.urlPattern) : cache.urlPattern,
|
|
819
|
+
handler,
|
|
820
|
+
options: cache.options ? {
|
|
821
|
+
cacheName: cache.options.cacheName,
|
|
822
|
+
expiration: cache.options.expiration ? {
|
|
823
|
+
maxEntries: cache.options.expiration.maxEntries,
|
|
824
|
+
maxAgeSeconds: cache.options.expiration.maxAgeSeconds
|
|
825
|
+
} : void 0
|
|
826
|
+
} : void 0
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
const result = await generateSW(workboxConfig);
|
|
832
|
+
return {
|
|
833
|
+
swPath: swDestPath,
|
|
834
|
+
count: result.count,
|
|
835
|
+
size: result.size,
|
|
836
|
+
warnings: result.warnings ?? [],
|
|
837
|
+
filePaths: (result.manifestEntries ?? []).map(
|
|
838
|
+
(entry) => typeof entry === "string" ? entry : entry.url
|
|
839
|
+
)
|
|
840
|
+
};
|
|
841
|
+
} catch (err) {
|
|
842
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
843
|
+
throw new Error(`Failed to generate service worker: ${message}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function generateAndWriteServiceWorker(options) {
|
|
847
|
+
return generateServiceWorker(options);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/generator/https-checker.ts
|
|
851
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
852
|
+
import { join as join7 } from "path";
|
|
853
|
+
function checkHttps(url, allowHttpLocalhost = true) {
|
|
854
|
+
let parsedUrl;
|
|
855
|
+
try {
|
|
856
|
+
parsedUrl = new URL(url);
|
|
857
|
+
} catch {
|
|
858
|
+
return {
|
|
859
|
+
isSecure: false,
|
|
860
|
+
isLocalhost: false,
|
|
861
|
+
isProduction: false,
|
|
862
|
+
protocol: "unknown",
|
|
863
|
+
hostname: null,
|
|
864
|
+
warning: "Invalid URL format",
|
|
865
|
+
recommendation: "Provide a valid URL (e.g., https://example.com or http://localhost:3000)"
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
const protocol = parsedUrl.protocol.replace(":", "");
|
|
869
|
+
const hostname = parsedUrl.hostname;
|
|
870
|
+
const isHttps = protocol === "https";
|
|
871
|
+
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.") || hostname.endsWith(".local");
|
|
872
|
+
const isSecure = isHttps || isLocalhost && allowHttpLocalhost;
|
|
873
|
+
const isProduction = isHttps && !isLocalhost;
|
|
874
|
+
let warning;
|
|
875
|
+
let recommendation;
|
|
876
|
+
if (!isSecure) {
|
|
877
|
+
if (isLocalhost && !allowHttpLocalhost) {
|
|
878
|
+
warning = "Service Workers require HTTPS in production. HTTP is only allowed on localhost for development.";
|
|
879
|
+
recommendation = "Use HTTPS in production or enable allowHttpLocalhost option for development.";
|
|
880
|
+
} else if (!isLocalhost) {
|
|
881
|
+
warning = "Service Workers require HTTPS in production. HTTP is not secure and will not work.";
|
|
882
|
+
recommendation = "Deploy your PWA with HTTPS. Consider using services like Vercel, Netlify, or Cloudflare that provide HTTPS by default.";
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
isSecure,
|
|
887
|
+
isLocalhost,
|
|
888
|
+
isProduction,
|
|
889
|
+
protocol,
|
|
890
|
+
hostname,
|
|
891
|
+
warning,
|
|
892
|
+
recommendation
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function detectProjectUrl(projectPath) {
|
|
896
|
+
const configFiles = [
|
|
897
|
+
{ file: "package.json", key: "homepage" },
|
|
898
|
+
{ file: "package.json", key: "url" },
|
|
899
|
+
{ file: ".env", pattern: /^.*URL.*=(.+)$/i },
|
|
900
|
+
{ file: ".env.local", pattern: /^.*URL.*=(.+)$/i },
|
|
901
|
+
{ file: "vercel.json", key: "url" },
|
|
902
|
+
{ file: "netlify.toml", pattern: /^.*url.*=.*["'](.+)["']/i },
|
|
903
|
+
{ file: "next.config.js", pattern: /baseUrl.*["'](.+)["']/ },
|
|
904
|
+
{ file: "next.config.ts", pattern: /baseUrl.*["'](.+)["']/ }
|
|
905
|
+
];
|
|
906
|
+
for (const config of configFiles) {
|
|
907
|
+
const filePath = join7(projectPath, config.file);
|
|
908
|
+
if (existsSync6(filePath)) {
|
|
909
|
+
try {
|
|
910
|
+
if (config.file.endsWith(".json") && config.key) {
|
|
911
|
+
const parsed = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
912
|
+
const content = parsed;
|
|
913
|
+
const value = content[config.key];
|
|
914
|
+
if (value && typeof value === "string") {
|
|
915
|
+
return value;
|
|
916
|
+
}
|
|
917
|
+
} else if (config.file.endsWith(".toml") && config.pattern) {
|
|
918
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
919
|
+
const match = config.pattern ? content.match(config.pattern) : null;
|
|
920
|
+
if (match && match[1]) {
|
|
921
|
+
return match[1];
|
|
922
|
+
}
|
|
923
|
+
} else if ((config.file.endsWith(".js") || config.file.endsWith(".ts")) && config.pattern) {
|
|
924
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
925
|
+
const match = config.pattern ? content.match(config.pattern) : null;
|
|
926
|
+
if (match && match[1]) {
|
|
927
|
+
return match[1];
|
|
928
|
+
}
|
|
929
|
+
} else if (config.file.startsWith(".env") && config.pattern) {
|
|
930
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
931
|
+
const lines = content.split("\n");
|
|
932
|
+
for (const line of lines) {
|
|
933
|
+
const match = config.pattern ? line.match(config.pattern) : null;
|
|
934
|
+
if (match && match[1]) {
|
|
935
|
+
return match[1].trim();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} catch {
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
function checkProjectHttps(options = {}) {
|
|
946
|
+
const { url, projectPath, allowHttpLocalhost = true } = options;
|
|
947
|
+
if (url) {
|
|
948
|
+
return checkHttps(url, allowHttpLocalhost);
|
|
949
|
+
}
|
|
950
|
+
if (projectPath) {
|
|
951
|
+
const detectedUrl = detectProjectUrl(projectPath);
|
|
952
|
+
if (detectedUrl) {
|
|
953
|
+
return checkHttps(detectedUrl, allowHttpLocalhost);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
isSecure: false,
|
|
958
|
+
isLocalhost: false,
|
|
959
|
+
isProduction: false,
|
|
960
|
+
protocol: "unknown",
|
|
961
|
+
hostname: null,
|
|
962
|
+
warning: "Unable to determine project URL. HTTPS check cannot be performed.",
|
|
963
|
+
recommendation: "Provide a URL explicitly or ensure your project has a valid configuration file (package.json, .env, etc.)."
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/injector/html-parser.ts
|
|
968
|
+
import { parseDocument } from "htmlparser2";
|
|
969
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
970
|
+
function parseHTMLFile(filePath, options = {}) {
|
|
971
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
972
|
+
return parseHTML(content, options);
|
|
973
|
+
}
|
|
974
|
+
function parseHTML(htmlContent, options = {}) {
|
|
975
|
+
const {
|
|
976
|
+
decodeEntities = true,
|
|
977
|
+
lowerCaseAttributeNames = true
|
|
978
|
+
} = options;
|
|
979
|
+
const document = parseDocument(htmlContent, {
|
|
980
|
+
decodeEntities,
|
|
981
|
+
lowerCaseAttributeNames,
|
|
982
|
+
lowerCaseTags: true
|
|
983
|
+
});
|
|
984
|
+
let head = null;
|
|
985
|
+
let body = null;
|
|
986
|
+
let html = null;
|
|
987
|
+
const findElements = (node) => {
|
|
988
|
+
if (node.type === "tag") {
|
|
989
|
+
const element = node;
|
|
990
|
+
const tagName = element.tagName.toLowerCase();
|
|
991
|
+
if (tagName === "head" && !head) {
|
|
992
|
+
head = element;
|
|
993
|
+
} else if (tagName === "body" && !body) {
|
|
994
|
+
body = element;
|
|
995
|
+
} else if (tagName === "html" && !html) {
|
|
996
|
+
html = element;
|
|
997
|
+
}
|
|
998
|
+
if (element.children) {
|
|
999
|
+
for (const child of element.children) {
|
|
1000
|
+
findElements(child);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
if (document.children) {
|
|
1006
|
+
for (const child of document.children) {
|
|
1007
|
+
findElements(child);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
document,
|
|
1012
|
+
head,
|
|
1013
|
+
body,
|
|
1014
|
+
html,
|
|
1015
|
+
originalContent: htmlContent
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function findElement(parsed, tagName, attribute) {
|
|
1019
|
+
const searchIn = parsed.head || parsed.document;
|
|
1020
|
+
const search = (node) => {
|
|
1021
|
+
if (node.type === "tag") {
|
|
1022
|
+
const element = node;
|
|
1023
|
+
if (element.tagName.toLowerCase() === tagName.toLowerCase()) {
|
|
1024
|
+
if (!attribute) {
|
|
1025
|
+
return element;
|
|
1026
|
+
}
|
|
1027
|
+
const attrValue = element.attributes?.find((attr) => attr.name.toLowerCase() === attribute.name.toLowerCase())?.value;
|
|
1028
|
+
if (attrValue === attribute.value) {
|
|
1029
|
+
return element;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (element.children) {
|
|
1033
|
+
for (const child of element.children) {
|
|
1034
|
+
const found = search(child);
|
|
1035
|
+
if (found) {
|
|
1036
|
+
return found;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
};
|
|
1043
|
+
if (searchIn) {
|
|
1044
|
+
if (searchIn.type === "tag") {
|
|
1045
|
+
return search(searchIn);
|
|
1046
|
+
}
|
|
1047
|
+
if ("children" in searchIn && searchIn.children) {
|
|
1048
|
+
for (const child of searchIn.children) {
|
|
1049
|
+
const found = search(child);
|
|
1050
|
+
if (found) {
|
|
1051
|
+
return found;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
function findAllElements(parsed, tagName, attribute) {
|
|
1059
|
+
const results = [];
|
|
1060
|
+
const targetTagName = tagName.toLowerCase();
|
|
1061
|
+
const search = (node, isRoot = false) => {
|
|
1062
|
+
if (node.type === "tag") {
|
|
1063
|
+
const element = node;
|
|
1064
|
+
const nodeTagName = element.tagName.toLowerCase();
|
|
1065
|
+
if (nodeTagName === targetTagName && (!isRoot || targetTagName === "html" || targetTagName === "head" || targetTagName === "body")) {
|
|
1066
|
+
if (!attribute) {
|
|
1067
|
+
results.push(element);
|
|
1068
|
+
} else {
|
|
1069
|
+
const attrValue = element.attributes?.find((attr) => attr.name.toLowerCase() === attribute.name.toLowerCase())?.value;
|
|
1070
|
+
if (attribute.value === void 0 || attrValue === attribute.value) {
|
|
1071
|
+
results.push(element);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (element.children) {
|
|
1076
|
+
for (const child of element.children) {
|
|
1077
|
+
const childElement = child;
|
|
1078
|
+
const childIsRoot = isRoot && childElement.type === "tag" && (childElement.tagName.toLowerCase() === "html" || childElement.tagName.toLowerCase() === "head" || childElement.tagName.toLowerCase() === "body");
|
|
1079
|
+
search(child, childIsRoot);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
if (parsed.head) {
|
|
1085
|
+
search(parsed.head, true);
|
|
1086
|
+
} else {
|
|
1087
|
+
if (parsed.document.children) {
|
|
1088
|
+
for (const child of parsed.document.children) {
|
|
1089
|
+
const childElement = child;
|
|
1090
|
+
const childIsRoot = childElement.type === "tag" && (childElement.tagName.toLowerCase() === "html" || childElement.tagName.toLowerCase() === "head" || childElement.tagName.toLowerCase() === "body");
|
|
1091
|
+
search(child, childIsRoot);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return results;
|
|
1096
|
+
}
|
|
1097
|
+
function elementExists(parsed, tagName, attribute) {
|
|
1098
|
+
return findElement(parsed, tagName, attribute) !== null;
|
|
1099
|
+
}
|
|
1100
|
+
function serializeHTML(parsed) {
|
|
1101
|
+
return parsed.originalContent;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/injector/meta-injector.ts
|
|
1105
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1106
|
+
import { render } from "dom-serializer";
|
|
1107
|
+
function injectMetaTags(htmlContent, options = {}) {
|
|
1108
|
+
const parsed = parseHTML(htmlContent);
|
|
1109
|
+
const result = {
|
|
1110
|
+
injected: [],
|
|
1111
|
+
skipped: [],
|
|
1112
|
+
warnings: []
|
|
1113
|
+
};
|
|
1114
|
+
let head = parsed.head;
|
|
1115
|
+
if (!head) {
|
|
1116
|
+
if (parsed.html) {
|
|
1117
|
+
const headElement = {
|
|
1118
|
+
type: "tag",
|
|
1119
|
+
name: "head",
|
|
1120
|
+
tagName: "head",
|
|
1121
|
+
attribs: {},
|
|
1122
|
+
children: [],
|
|
1123
|
+
parent: parsed.html,
|
|
1124
|
+
next: null,
|
|
1125
|
+
prev: null
|
|
1126
|
+
};
|
|
1127
|
+
if (parsed.html.children) {
|
|
1128
|
+
parsed.html.children.unshift(headElement);
|
|
1129
|
+
} else {
|
|
1130
|
+
parsed.html.children = [headElement];
|
|
1131
|
+
}
|
|
1132
|
+
head = headElement;
|
|
1133
|
+
result.warnings.push("Created <head> tag (was missing)");
|
|
1134
|
+
} else {
|
|
1135
|
+
result.warnings.push("No <html> or <head> tag found, meta tags may not be injected correctly");
|
|
1136
|
+
return { html: htmlContent, result };
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (options.manifestPath) {
|
|
1140
|
+
const manifestHref = options.manifestPath.startsWith("/") ? options.manifestPath : `/${options.manifestPath}`;
|
|
1141
|
+
if (!elementExists(parsed, "link", { name: "rel", value: "manifest" })) {
|
|
1142
|
+
injectLinkTag(head, "manifest", manifestHref);
|
|
1143
|
+
result.injected.push(`<link rel="manifest" href="${manifestHref}">`);
|
|
1144
|
+
} else {
|
|
1145
|
+
result.skipped.push("manifest link (already exists)");
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (options.themeColor) {
|
|
1149
|
+
const existingThemeColor = findElement(parsed, "meta", { name: "name", value: "theme-color" });
|
|
1150
|
+
if (existingThemeColor) {
|
|
1151
|
+
updateMetaContent(existingThemeColor, options.themeColor);
|
|
1152
|
+
result.injected.push(`<meta name="theme-color" content="${options.themeColor}"> (updated)`);
|
|
1153
|
+
} else if (!elementExists(parsed, "meta", { name: "theme-color", value: options.themeColor })) {
|
|
1154
|
+
injectMetaTag(head, "theme-color", options.themeColor);
|
|
1155
|
+
result.injected.push(`<meta name="theme-color" content="${options.themeColor}">`);
|
|
1156
|
+
} else {
|
|
1157
|
+
result.skipped.push("theme-color (already exists)");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (options.appleTouchIcon) {
|
|
1161
|
+
const iconHref = options.appleTouchIcon.startsWith("/") ? options.appleTouchIcon : `/${options.appleTouchIcon}`;
|
|
1162
|
+
if (!elementExists(parsed, "link", { name: "rel", value: "apple-touch-icon" })) {
|
|
1163
|
+
injectLinkTag(head, "apple-touch-icon", iconHref);
|
|
1164
|
+
result.injected.push(`<link rel="apple-touch-icon" href="${iconHref}">`);
|
|
1165
|
+
} else {
|
|
1166
|
+
result.skipped.push("apple-touch-icon (already exists)");
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (options.appleMobileWebAppCapable !== void 0) {
|
|
1170
|
+
const content = options.appleMobileWebAppCapable ? "yes" : "no";
|
|
1171
|
+
if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-capable", value: content })) {
|
|
1172
|
+
injectMetaTag(head, "apple-mobile-web-app-capable", content);
|
|
1173
|
+
result.injected.push(`<meta name="apple-mobile-web-app-capable" content="${content}">`);
|
|
1174
|
+
} else {
|
|
1175
|
+
result.skipped.push("apple-mobile-web-app-capable (already exists)");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (options.appleMobileWebAppStatusBarStyle) {
|
|
1179
|
+
if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-status-bar-style", value: options.appleMobileWebAppStatusBarStyle })) {
|
|
1180
|
+
injectMetaTag(head, "apple-mobile-web-app-status-bar-style", options.appleMobileWebAppStatusBarStyle);
|
|
1181
|
+
result.injected.push(`<meta name="apple-mobile-web-app-status-bar-style" content="${options.appleMobileWebAppStatusBarStyle}">`);
|
|
1182
|
+
} else {
|
|
1183
|
+
result.skipped.push("apple-mobile-web-app-status-bar-style (already exists)");
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (options.appleMobileWebAppTitle) {
|
|
1187
|
+
if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-title", value: options.appleMobileWebAppTitle })) {
|
|
1188
|
+
injectMetaTag(head, "apple-mobile-web-app-title", options.appleMobileWebAppTitle);
|
|
1189
|
+
result.injected.push(`<meta name="apple-mobile-web-app-title" content="${options.appleMobileWebAppTitle}">`);
|
|
1190
|
+
} else {
|
|
1191
|
+
result.skipped.push("apple-mobile-web-app-title (already exists)");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const modifiedHtml = render(parsed.document, { decodeEntities: false });
|
|
1195
|
+
if (options.serviceWorkerPath) {
|
|
1196
|
+
const swPath = options.serviceWorkerPath.startsWith("/") ? options.serviceWorkerPath : `/${options.serviceWorkerPath}`;
|
|
1197
|
+
if (!htmlContent.includes("navigator.serviceWorker")) {
|
|
1198
|
+
const swScript = `
|
|
1199
|
+
<script>
|
|
1200
|
+
if ('serviceWorker' in navigator) {
|
|
1201
|
+
window.addEventListener('load', () => {
|
|
1202
|
+
navigator.serviceWorker.register('${swPath}')
|
|
1203
|
+
.then((registration) => console.log('SW registered:', registration))
|
|
1204
|
+
.catch((error) => console.error('SW registration failed:', error));
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
</script>`;
|
|
1208
|
+
const finalHtml = modifiedHtml.replace("</body>", `${swScript}
|
|
1209
|
+
</body>`);
|
|
1210
|
+
result.injected.push("Service Worker registration script");
|
|
1211
|
+
return { html: finalHtml, result };
|
|
1212
|
+
} else {
|
|
1213
|
+
result.skipped.push("Service Worker registration (already exists)");
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return { html: modifiedHtml, result };
|
|
1217
|
+
}
|
|
1218
|
+
function injectLinkTag(head, rel, href) {
|
|
1219
|
+
const linkElement = {
|
|
1220
|
+
type: "tag",
|
|
1221
|
+
name: "link",
|
|
1222
|
+
tagName: "link",
|
|
1223
|
+
attribs: {
|
|
1224
|
+
rel,
|
|
1225
|
+
href
|
|
1226
|
+
},
|
|
1227
|
+
children: [],
|
|
1228
|
+
parent: head,
|
|
1229
|
+
next: null,
|
|
1230
|
+
prev: null
|
|
1231
|
+
};
|
|
1232
|
+
if (!head.children) {
|
|
1233
|
+
head.children = [];
|
|
1234
|
+
}
|
|
1235
|
+
head.children.push(linkElement);
|
|
1236
|
+
}
|
|
1237
|
+
function injectMetaTag(head, name, content) {
|
|
1238
|
+
const metaElement = {
|
|
1239
|
+
type: "tag",
|
|
1240
|
+
name: "meta",
|
|
1241
|
+
tagName: "meta",
|
|
1242
|
+
attribs: {
|
|
1243
|
+
name,
|
|
1244
|
+
content
|
|
1245
|
+
},
|
|
1246
|
+
children: [],
|
|
1247
|
+
parent: head,
|
|
1248
|
+
next: null,
|
|
1249
|
+
prev: null
|
|
1250
|
+
};
|
|
1251
|
+
if (!head.children) {
|
|
1252
|
+
head.children = [];
|
|
1253
|
+
}
|
|
1254
|
+
head.children.push(metaElement);
|
|
1255
|
+
}
|
|
1256
|
+
function updateMetaContent(metaElement, newContent) {
|
|
1257
|
+
if (metaElement.attribs) {
|
|
1258
|
+
metaElement.attribs.content = newContent;
|
|
1259
|
+
} else {
|
|
1260
|
+
metaElement.attribs = { content: newContent };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function injectMetaTagsInFile(filePath, options = {}) {
|
|
1264
|
+
const parsed = parseHTMLFile(filePath);
|
|
1265
|
+
const { html, result } = injectMetaTags(parsed.originalContent, options);
|
|
1266
|
+
writeFileSync3(filePath, html, "utf-8");
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1269
|
+
export {
|
|
1270
|
+
ManifestSchema,
|
|
1271
|
+
STANDARD_ICON_SIZES,
|
|
1272
|
+
STANDARD_SPLASH_SIZES,
|
|
1273
|
+
checkHttps,
|
|
1274
|
+
checkProjectHttps,
|
|
1275
|
+
detectArchitecture,
|
|
1276
|
+
detectAssets,
|
|
1277
|
+
detectFramework,
|
|
1278
|
+
detectProjectUrl,
|
|
1279
|
+
elementExists,
|
|
1280
|
+
findAllElements,
|
|
1281
|
+
findElement,
|
|
1282
|
+
generateAndWriteManifest,
|
|
1283
|
+
generateAndWriteServiceWorker,
|
|
1284
|
+
generateAppleTouchIcon,
|
|
1285
|
+
generateFavicon,
|
|
1286
|
+
generateIcons,
|
|
1287
|
+
generateIconsOnly,
|
|
1288
|
+
generateManifest,
|
|
1289
|
+
generateReport,
|
|
1290
|
+
generateServiceWorker,
|
|
1291
|
+
generateSimpleServiceWorker,
|
|
1292
|
+
generateSplashScreensOnly,
|
|
1293
|
+
injectMetaTags,
|
|
1294
|
+
injectMetaTagsInFile,
|
|
1295
|
+
parseHTML,
|
|
1296
|
+
parseHTMLFile,
|
|
1297
|
+
scanProject,
|
|
1298
|
+
serializeHTML,
|
|
1299
|
+
validateProjectPath,
|
|
1300
|
+
writeManifest
|
|
1301
|
+
};
|