@ahmedrowaihi/pdf-forge-cli 1.0.0-canary.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/.turbo/turbo-build.log +14 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE.md +8 -0
- package/dist/index.js +1058 -0
- package/package.json +60 -0
- package/src/commands/build.ts +249 -0
- package/src/commands/dev.ts +27 -0
- package/src/commands/export.ts +204 -0
- package/src/commands/start.ts +35 -0
- package/src/index.ts +67 -0
- package/src/utils/conf.ts +10 -0
- package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +62 -0
- package/src/utils/get-preview-server-location.ts +108 -0
- package/src/utils/get-templates-directory-metadata.ts +139 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/packageJson.ts +4 -0
- package/src/utils/preview/get-env-variables-for-preview-app.ts +17 -0
- package/src/utils/preview/hot-reloading/create-dependency-graph.ts +345 -0
- package/src/utils/preview/hot-reloading/get-imported-modules.ts +49 -0
- package/src/utils/preview/hot-reloading/resolve-path-aliases.ts +32 -0
- package/src/utils/preview/hot-reloading/setup-hot-reloading.ts +125 -0
- package/src/utils/preview/serve-static-file.ts +134 -0
- package/src/utils/preview/start-dev-server.ts +242 -0
- package/src/utils/register-spinner-autostopping.ts +29 -0
- package/src/utils/style-text.ts +11 -0
- package/src/utils/tree.ts +76 -0
- package/tsconfig.json +38 -0
- package/tsdown.config.ts +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import fs, { existsSync, promises, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import logSymbols from "log-symbols";
|
|
7
|
+
import { addDevDependency, installDependencies, runScript } from "nypm";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import url from "node:url";
|
|
10
|
+
import { createJiti } from "jiti";
|
|
11
|
+
import prompts from "prompts";
|
|
12
|
+
import { watch } from "chokidar";
|
|
13
|
+
import debounce from "debounce";
|
|
14
|
+
import { Server } from "socket.io";
|
|
15
|
+
import { parse } from "@babel/parser";
|
|
16
|
+
import traverseModule from "@babel/traverse";
|
|
17
|
+
import { createMatchPath, loadConfig } from "tsconfig-paths";
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
import * as nodeUtil from "node:util";
|
|
20
|
+
import { lookup } from "mime-types";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import { build } from "esbuild";
|
|
23
|
+
import { glob } from "glob";
|
|
24
|
+
import normalize from "normalize-path";
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
|
|
27
|
+
//#region package.json
|
|
28
|
+
var package_default = {
|
|
29
|
+
name: "@ahmedrowaihi/pdf-forge-cli",
|
|
30
|
+
version: "1.0.0-canary.0",
|
|
31
|
+
description: "A live preview of your PDF templates right in your browser.",
|
|
32
|
+
bin: { "pdf-dev": "./dist/index.js" },
|
|
33
|
+
type: "module",
|
|
34
|
+
scripts: {
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"build:watch": "tsdown --watch src",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
},
|
|
41
|
+
license: "MIT",
|
|
42
|
+
repository: {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/ahmedrowaihi/react-pdf-forge.git",
|
|
45
|
+
"directory": "packages/react-pdf"
|
|
46
|
+
},
|
|
47
|
+
keywords: ["react", "pdf"],
|
|
48
|
+
engines: { "node": ">=20.0.0" },
|
|
49
|
+
dependencies: {
|
|
50
|
+
"@babel/parser": "^7.27.0",
|
|
51
|
+
"@babel/traverse": "^7.27.0",
|
|
52
|
+
"chokidar": "^4.0.3",
|
|
53
|
+
"commander": "^13.0.0",
|
|
54
|
+
"conf": "^15.0.2",
|
|
55
|
+
"debounce": "^2.0.0",
|
|
56
|
+
"esbuild": "^0.25.0",
|
|
57
|
+
"glob": "^11.0.0",
|
|
58
|
+
"jiti": "2.4.2",
|
|
59
|
+
"log-symbols": "^7.0.0",
|
|
60
|
+
"mime-types": "^3.0.0",
|
|
61
|
+
"normalize-path": "^3.0.0",
|
|
62
|
+
"nypm": "0.6.2",
|
|
63
|
+
"ora": "^8.0.0",
|
|
64
|
+
"prompts": "2.4.2",
|
|
65
|
+
"socket.io": "^4.8.1",
|
|
66
|
+
"tsconfig-paths": "4.2.0"
|
|
67
|
+
},
|
|
68
|
+
devDependencies: {
|
|
69
|
+
"@ahmedrowaihi/pdf-forge-components": "workspace:*",
|
|
70
|
+
"@ahmedrowaihi/pdf-forge-core": "workspace:*",
|
|
71
|
+
"@types/babel__core": "7.20.5",
|
|
72
|
+
"@types/babel__traverse": "7.20.7",
|
|
73
|
+
"@types/mime-types": "2.1.4",
|
|
74
|
+
"@types/prompts": "2.4.9",
|
|
75
|
+
"next": "^16",
|
|
76
|
+
"react": "^19",
|
|
77
|
+
"react-dom": "^19",
|
|
78
|
+
"typescript": "5.8.3"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/utils/get-preview-server-location.ts
|
|
84
|
+
const ensurePreviewServerInstalled = async (message) => {
|
|
85
|
+
if ((await prompts({
|
|
86
|
+
type: "confirm",
|
|
87
|
+
name: "installPreviewServer",
|
|
88
|
+
message,
|
|
89
|
+
initial: true
|
|
90
|
+
})).installPreviewServer) {
|
|
91
|
+
console.log("Installing \"@ahmedrowaihi/pdf-forge-preview\"");
|
|
92
|
+
await addDevDependency(`@ahmedrowaihi/pdf-forge-preview@${package_default.version}`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
} else process.exit(0);
|
|
95
|
+
};
|
|
96
|
+
const findWorkspacePreviewServer = () => {
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
let workspaceRoot = null;
|
|
99
|
+
let currentPath = cwd;
|
|
100
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
101
|
+
const pnpmWorkspace = path.join(currentPath, "pnpm-workspace.yaml");
|
|
102
|
+
if (fs.existsSync(pnpmWorkspace)) {
|
|
103
|
+
workspaceRoot = currentPath;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
currentPath = path.dirname(currentPath);
|
|
107
|
+
}
|
|
108
|
+
if (workspaceRoot) {
|
|
109
|
+
const previewServerPath = path.resolve(workspaceRoot, "packages/preview-server");
|
|
110
|
+
const packageJsonPath = path.join(previewServerPath, "package.json");
|
|
111
|
+
if (fs.existsSync(packageJsonPath)) try {
|
|
112
|
+
if (JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).name === "@ahmedrowaihi/pdf-forge-preview") return previewServerPath;
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
const getPreviewServerLocation = async () => {
|
|
118
|
+
const usersProject = createJiti(process.cwd());
|
|
119
|
+
let previewServerLocation;
|
|
120
|
+
const workspacePath = findWorkspacePreviewServer();
|
|
121
|
+
if (workspacePath) previewServerLocation = workspacePath;
|
|
122
|
+
else try {
|
|
123
|
+
previewServerLocation = path.dirname(url.fileURLToPath(usersProject.esmResolve("@ahmedrowaihi/pdf-forge-preview")));
|
|
124
|
+
} catch {
|
|
125
|
+
await ensurePreviewServerInstalled("To run the preview server, the package \"@ahmedrowaihi/pdf-forge-preview\" must be installed. Would you like to install it?");
|
|
126
|
+
}
|
|
127
|
+
if (!workspacePath) try {
|
|
128
|
+
const { version: version$1 } = await usersProject.import("@ahmedrowaihi/pdf-forge-preview");
|
|
129
|
+
if (version$1 !== package_default.version) await ensurePreviewServerInstalled(`To run the preview server, the version of "@ahmedrowaihi/pdf-forge-preview" must match the version of "@ahmedrowaihi/pdf-forge-cli" (${package_default.version}). Would you like to install it?`);
|
|
130
|
+
} catch {
|
|
131
|
+
await ensurePreviewServerInstalled("To run the preview server, the package \"@ahmedrowaihi/pdf-forge-preview\" must be installed. Would you like to install it?");
|
|
132
|
+
}
|
|
133
|
+
return previewServerLocation;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/utils/get-templates-directory-metadata.ts
|
|
138
|
+
const isFileATemplate = async (fullPath) => {
|
|
139
|
+
let fileHandle;
|
|
140
|
+
try {
|
|
141
|
+
fileHandle = await fs.promises.open(fullPath, "r");
|
|
142
|
+
} catch (exception) {
|
|
143
|
+
console.warn(exception);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if ((await fileHandle.stat()).isDirectory()) {
|
|
147
|
+
await fileHandle.close();
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const { ext } = path.parse(fullPath);
|
|
151
|
+
if (![
|
|
152
|
+
".js",
|
|
153
|
+
".tsx",
|
|
154
|
+
".jsx"
|
|
155
|
+
].includes(ext)) {
|
|
156
|
+
await fileHandle.close();
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const fileContents = await fileHandle.readFile("utf8");
|
|
160
|
+
await fileHandle.close();
|
|
161
|
+
const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
|
|
162
|
+
const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
|
|
163
|
+
const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(fileContents);
|
|
164
|
+
return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
|
|
165
|
+
};
|
|
166
|
+
const mergeDirectoriesWithSubDirectories = (templatesDirectoryMetadata) => {
|
|
167
|
+
let currentResultingMergedDirectory = templatesDirectoryMetadata;
|
|
168
|
+
while (currentResultingMergedDirectory.templateFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
|
|
169
|
+
const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
|
|
170
|
+
currentResultingMergedDirectory = {
|
|
171
|
+
...onlySubDirectory,
|
|
172
|
+
directoryName: path.join(currentResultingMergedDirectory.directoryName, onlySubDirectory.directoryName)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return currentResultingMergedDirectory;
|
|
176
|
+
};
|
|
177
|
+
const getTemplatesDirectoryMetadata = async (absolutePathToTemplatesDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToTemplatesDirectory) => {
|
|
178
|
+
if (!fs.existsSync(absolutePathToTemplatesDirectory)) return;
|
|
179
|
+
const dirents = await fs.promises.readdir(absolutePathToTemplatesDirectory, { withFileTypes: true });
|
|
180
|
+
const isTemplatePredicates = await Promise.all(dirents.map((dirent) => isFileATemplate(path.join(absolutePathToTemplatesDirectory, dirent.name))));
|
|
181
|
+
const templateFilenames = dirents.filter((_, i) => isTemplatePredicates[i]).map((dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), ""));
|
|
182
|
+
const subDirectories = await Promise.all(dirents.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static").map((dirent) => {
|
|
183
|
+
return getTemplatesDirectoryMetadata(path.join(absolutePathToTemplatesDirectory, dirent.name), keepFileExtensions, true, baseDirectoryPath);
|
|
184
|
+
}));
|
|
185
|
+
const templatesMetadata = {
|
|
186
|
+
absolutePath: absolutePathToTemplatesDirectory,
|
|
187
|
+
relativePath: path.relative(baseDirectoryPath, absolutePathToTemplatesDirectory),
|
|
188
|
+
directoryName: absolutePathToTemplatesDirectory.split(path.sep).pop(),
|
|
189
|
+
templateFilenames,
|
|
190
|
+
subDirectories
|
|
191
|
+
};
|
|
192
|
+
return isSubDirectory ? mergeDirectoriesWithSubDirectories(templatesMetadata) : templatesMetadata;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/utils/register-spinner-autostopping.ts
|
|
197
|
+
const spinners = /* @__PURE__ */ new Set();
|
|
198
|
+
process.on("SIGINT", () => {
|
|
199
|
+
spinners.forEach((spinner) => {
|
|
200
|
+
if (spinner.isSpinning) spinner.stop();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
process.on("exit", (code) => {
|
|
204
|
+
if (code !== 0) spinners.forEach((spinner) => {
|
|
205
|
+
if (spinner.isSpinning) spinner.stopAndPersist({
|
|
206
|
+
symbol: logSymbols.error,
|
|
207
|
+
text: "Process exited with error"
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
const registerSpinnerAutostopping = (spinner) => {
|
|
212
|
+
spinners.add(spinner);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/commands/build.ts
|
|
217
|
+
const setNextEnvironmentVariablesForBuild = async (templatesDirRelativePath, builtPreviewAppPath) => {
|
|
218
|
+
const nextConfigContents = `
|
|
219
|
+
import path from 'path';
|
|
220
|
+
const templatesDirRelativePath = path.normalize('${templatesDirRelativePath}');
|
|
221
|
+
const userProjectLocation = '${process.cwd().replace(/\\/g, "/")}';
|
|
222
|
+
const previewServerLocation = '${builtPreviewAppPath.replace(/\\/g, "/")}';
|
|
223
|
+
/** @type {import('next').NextConfig} */
|
|
224
|
+
const nextConfig = {
|
|
225
|
+
env: {
|
|
226
|
+
NEXT_PUBLIC_IS_BUILDING: 'true',
|
|
227
|
+
TEMPLATES_DIR_RELATIVE_PATH: templatesDirRelativePath,
|
|
228
|
+
TEMPLATES_DIR_ABSOLUTE_PATH: path.resolve(userProjectLocation, templatesDirRelativePath),
|
|
229
|
+
PREVIEW_SERVER_LOCATION: previewServerLocation,
|
|
230
|
+
USER_PROJECT_LOCATION: userProjectLocation
|
|
231
|
+
},
|
|
232
|
+
outputFileTracingRoot: previewServerLocation,
|
|
233
|
+
serverExternalPackages: ['esbuild'],
|
|
234
|
+
typescript: {
|
|
235
|
+
ignoreBuildErrors: true
|
|
236
|
+
},
|
|
237
|
+
experimental: {
|
|
238
|
+
webpackBuildWorker: true
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default nextConfig`;
|
|
243
|
+
await fs.promises.writeFile(path.resolve(builtPreviewAppPath, "./next.config.mjs"), nextConfigContents, "utf8");
|
|
244
|
+
};
|
|
245
|
+
const getTemplateSlugsFromTemplateDirectory = (templateDirectory, templatesDirectoryAbsolutePath) => {
|
|
246
|
+
const directoryPathRelativeToTemplatesDirectory = templateDirectory.absolutePath.replace(templatesDirectoryAbsolutePath, "").trim();
|
|
247
|
+
const slugs = [];
|
|
248
|
+
for (const filename of templateDirectory.templateFilenames) slugs.push(path.join(directoryPathRelativeToTemplatesDirectory, filename).split(path.sep).filter((segment) => segment.length > 0));
|
|
249
|
+
for (const directory of templateDirectory.subDirectories) slugs.push(...getTemplateSlugsFromTemplateDirectory(directory, templatesDirectoryAbsolutePath));
|
|
250
|
+
return slugs;
|
|
251
|
+
};
|
|
252
|
+
const forceSSGForPDFPreviews = async (templatesDirPath, builtPreviewAppPath) => {
|
|
253
|
+
const parameters = getTemplateSlugsFromTemplateDirectory(await getTemplatesDirectoryMetadata(templatesDirPath), templatesDirPath).map((slug) => ({ slug }));
|
|
254
|
+
const removeForceDynamic = async (filePath) => {
|
|
255
|
+
const contents = await fs.promises.readFile(filePath, "utf8");
|
|
256
|
+
await fs.promises.writeFile(filePath, contents.replace("export const dynamic = 'force-dynamic';", ""), "utf8");
|
|
257
|
+
};
|
|
258
|
+
await removeForceDynamic(path.resolve(builtPreviewAppPath, "./src/app/layout.tsx"));
|
|
259
|
+
await removeForceDynamic(path.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"));
|
|
260
|
+
await fs.promises.appendFile(path.resolve(builtPreviewAppPath, "./src/app/preview/[...slug]/page.tsx"), `
|
|
261
|
+
|
|
262
|
+
export function generateStaticParams() {
|
|
263
|
+
return Promise.resolve(
|
|
264
|
+
${JSON.stringify(parameters)}
|
|
265
|
+
);
|
|
266
|
+
}`, "utf8");
|
|
267
|
+
};
|
|
268
|
+
const updatePackageJson = async (builtPreviewAppPath) => {
|
|
269
|
+
const packageJsonPath = path.resolve(builtPreviewAppPath, "./package.json");
|
|
270
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf8"));
|
|
271
|
+
packageJson.scripts.build = "next build";
|
|
272
|
+
packageJson.scripts.start = "next start";
|
|
273
|
+
delete packageJson.scripts.postbuild;
|
|
274
|
+
packageJson.name = "preview-server";
|
|
275
|
+
for (const [dependency, version$1] of Object.entries(packageJson.devDependencies)) packageJson.devDependencies[dependency] = version$1.replace("workspace:", "");
|
|
276
|
+
delete packageJson.devDependencies["@ahmedrowaihi/pdf-forge-components"];
|
|
277
|
+
delete packageJson.scripts.prepare;
|
|
278
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson), "utf8");
|
|
279
|
+
};
|
|
280
|
+
const build$1 = async ({ dir: templatesDirRelativePath, packageManager }) => {
|
|
281
|
+
try {
|
|
282
|
+
const previewServerLocation = await getPreviewServerLocation();
|
|
283
|
+
const spinner = ora({
|
|
284
|
+
text: "Starting build process...",
|
|
285
|
+
prefixText: " "
|
|
286
|
+
}).start();
|
|
287
|
+
registerSpinnerAutostopping(spinner);
|
|
288
|
+
spinner.text = `Checking if ${templatesDirRelativePath} folder exists`;
|
|
289
|
+
if (!fs.existsSync(templatesDirRelativePath)) process.exit(1);
|
|
290
|
+
const templatesDirPath = path.join(process.cwd(), templatesDirRelativePath);
|
|
291
|
+
const staticPath = path.join(templatesDirPath, "static");
|
|
292
|
+
const builtPreviewAppPath = path.join(process.cwd(), ".react-pdf");
|
|
293
|
+
if (fs.existsSync(builtPreviewAppPath)) {
|
|
294
|
+
spinner.text = "Deleting pre-existing `.react-pdf` folder";
|
|
295
|
+
await fs.promises.rm(builtPreviewAppPath, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
spinner.text = "Copying preview app from CLI to `.react-pdf`";
|
|
298
|
+
await fs.promises.cp(previewServerLocation, builtPreviewAppPath, {
|
|
299
|
+
recursive: true,
|
|
300
|
+
filter: (source) => {
|
|
301
|
+
return !/(\/|\\)cli(\/|\\)?/.test(source) && !/(\/|\\)\.next(\/|\\)?/.test(source) && !/(\/|\\)\.turbo(\/|\\)?/.test(source) && !/(\/|\\)node_modules(\/|\\)?$/.test(source);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
if (fs.existsSync(staticPath)) {
|
|
305
|
+
spinner.text = "Copying `static` folder into `.react-pdf/public/static`";
|
|
306
|
+
const builtStaticDirectory = path.resolve(builtPreviewAppPath, "./public/static");
|
|
307
|
+
await fs.promises.cp(staticPath, builtStaticDirectory, { recursive: true });
|
|
308
|
+
}
|
|
309
|
+
spinner.text = "Setting Next environment variables for preview app to work properly";
|
|
310
|
+
await setNextEnvironmentVariablesForBuild(templatesDirRelativePath, builtPreviewAppPath);
|
|
311
|
+
spinner.text = "Setting server side generation for the PDF preview pages";
|
|
312
|
+
await forceSSGForPDFPreviews(templatesDirPath, builtPreviewAppPath);
|
|
313
|
+
spinner.text = "Updating package.json's build and start scripts";
|
|
314
|
+
await updatePackageJson(builtPreviewAppPath);
|
|
315
|
+
spinner.text = "Installing dependencies on `.react-pdf`";
|
|
316
|
+
await installDependencies({
|
|
317
|
+
cwd: builtPreviewAppPath,
|
|
318
|
+
silent: true,
|
|
319
|
+
packageManager
|
|
320
|
+
});
|
|
321
|
+
spinner.stopAndPersist({
|
|
322
|
+
text: "Successfully prepared `.react-pdf` for `next build`",
|
|
323
|
+
symbol: logSymbols.success
|
|
324
|
+
});
|
|
325
|
+
await runScript("build", {
|
|
326
|
+
packageManager,
|
|
327
|
+
cwd: builtPreviewAppPath
|
|
328
|
+
});
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.log(error);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/utils/preview/hot-reloading/get-imported-modules.ts
|
|
337
|
+
const traverse = typeof traverseModule === "function" ? traverseModule : traverseModule.default;
|
|
338
|
+
const getImportedModules = (contents) => {
|
|
339
|
+
const importedPaths = [];
|
|
340
|
+
traverse(parse(contents, {
|
|
341
|
+
sourceType: "unambiguous",
|
|
342
|
+
strictMode: false,
|
|
343
|
+
errorRecovery: true,
|
|
344
|
+
plugins: [
|
|
345
|
+
"jsx",
|
|
346
|
+
"typescript",
|
|
347
|
+
"decorators"
|
|
348
|
+
]
|
|
349
|
+
}), {
|
|
350
|
+
ImportDeclaration({ node }) {
|
|
351
|
+
importedPaths.push(node.source.value);
|
|
352
|
+
},
|
|
353
|
+
ExportAllDeclaration({ node }) {
|
|
354
|
+
importedPaths.push(node.source.value);
|
|
355
|
+
},
|
|
356
|
+
ExportNamedDeclaration({ node }) {
|
|
357
|
+
if (node.source) importedPaths.push(node.source.value);
|
|
358
|
+
},
|
|
359
|
+
TSExternalModuleReference({ node }) {
|
|
360
|
+
importedPaths.push(node.expression.value);
|
|
361
|
+
},
|
|
362
|
+
CallExpression({ node }) {
|
|
363
|
+
if ("name" in node.callee && node.callee.name === "require") {
|
|
364
|
+
if (node.arguments.length === 1) {
|
|
365
|
+
const importPathNode = node.arguments[0];
|
|
366
|
+
if (importPathNode.type === "StringLiteral") importedPaths.push(importPathNode.value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return importedPaths;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/utils/preview/hot-reloading/resolve-path-aliases.ts
|
|
376
|
+
const resolvePathAliases = (importPaths, projectPath) => {
|
|
377
|
+
const configLoadResult = loadConfig(projectPath);
|
|
378
|
+
if (configLoadResult.resultType === "success") {
|
|
379
|
+
const matchPath = createMatchPath(configLoadResult.absoluteBaseUrl, configLoadResult.paths);
|
|
380
|
+
return importPaths.map((importedPath) => {
|
|
381
|
+
const unaliasedPath = matchPath(importedPath, void 0, void 0, [
|
|
382
|
+
".tsx",
|
|
383
|
+
".ts",
|
|
384
|
+
".js",
|
|
385
|
+
".jsx",
|
|
386
|
+
".cjs",
|
|
387
|
+
".mjs"
|
|
388
|
+
]);
|
|
389
|
+
if (unaliasedPath) return `./${path.relative(projectPath, unaliasedPath)}`;
|
|
390
|
+
return importedPath;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return importPaths;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/utils/preview/hot-reloading/create-dependency-graph.ts
|
|
398
|
+
const readAllFilesInsideDirectory = async (directory) => {
|
|
399
|
+
let allFilePaths = [];
|
|
400
|
+
const topLevelDirents = await promises.readdir(directory, { withFileTypes: true });
|
|
401
|
+
for (const dirent of topLevelDirents) {
|
|
402
|
+
const pathToDirent = path.join(directory, dirent.name);
|
|
403
|
+
if (dirent.isDirectory()) allFilePaths = allFilePaths.concat(await readAllFilesInsideDirectory(pathToDirent));
|
|
404
|
+
else allFilePaths.push(pathToDirent);
|
|
405
|
+
}
|
|
406
|
+
return allFilePaths;
|
|
407
|
+
};
|
|
408
|
+
const javascriptExtensions = [
|
|
409
|
+
".js",
|
|
410
|
+
".ts",
|
|
411
|
+
".jsx",
|
|
412
|
+
".tsx",
|
|
413
|
+
".mjs",
|
|
414
|
+
".cjs"
|
|
415
|
+
];
|
|
416
|
+
const isJavascriptModule = (filePath) => {
|
|
417
|
+
const extensionName = path.extname(filePath);
|
|
418
|
+
return javascriptExtensions.includes(extensionName);
|
|
419
|
+
};
|
|
420
|
+
const checkFileExtensionsUntilItExists = (pathWithoutExtension) => {
|
|
421
|
+
if (existsSync(`${pathWithoutExtension}.ts`)) return `${pathWithoutExtension}.ts`;
|
|
422
|
+
if (existsSync(`${pathWithoutExtension}.tsx`)) return `${pathWithoutExtension}.tsx`;
|
|
423
|
+
if (existsSync(`${pathWithoutExtension}.js`)) return `${pathWithoutExtension}.js`;
|
|
424
|
+
if (existsSync(`${pathWithoutExtension}.jsx`)) return `${pathWithoutExtension}.jsx`;
|
|
425
|
+
if (existsSync(`${pathWithoutExtension}.mjs`)) return `${pathWithoutExtension}.mjs`;
|
|
426
|
+
if (existsSync(`${pathWithoutExtension}.cjs`)) return `${pathWithoutExtension}.cjs`;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Creates a stateful dependency graph that is structured in a way that you can get
|
|
430
|
+
* the dependents of a module from its path.
|
|
431
|
+
*
|
|
432
|
+
* Stateful in the sense that it provides a `getter` and an "`updater`". The updater
|
|
433
|
+
* will receive changes to the files, that can be perceived through some file watching mechanism,
|
|
434
|
+
* so that it doesn't need to recompute the entire dependency graph but only the parts changed.
|
|
435
|
+
*/
|
|
436
|
+
const createDependencyGraph = async (directory) => {
|
|
437
|
+
const modulePaths = (await readAllFilesInsideDirectory(directory)).filter(isJavascriptModule);
|
|
438
|
+
const graph = Object.fromEntries(modulePaths.map((path$1) => [path$1, {
|
|
439
|
+
path: path$1,
|
|
440
|
+
dependencyPaths: [],
|
|
441
|
+
dependentPaths: [],
|
|
442
|
+
moduleDependencies: []
|
|
443
|
+
}]));
|
|
444
|
+
const getDependencyPaths = async (filePath) => {
|
|
445
|
+
const contents = await promises.readFile(filePath, "utf8");
|
|
446
|
+
const importedPathsRelativeToDirectory = (isJavascriptModule(filePath) ? resolvePathAliases(getImportedModules(contents), path.dirname(filePath)) : []).map((dependencyPath) => {
|
|
447
|
+
if (!dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)) return dependencyPath;
|
|
448
|
+
let pathToDependencyFromDirectory = path.resolve(path.dirname(filePath), dependencyPath);
|
|
449
|
+
let isDirectory = false;
|
|
450
|
+
try {
|
|
451
|
+
isDirectory = statSync(pathToDependencyFromDirectory).isDirectory();
|
|
452
|
+
} catch {}
|
|
453
|
+
if (isDirectory) {
|
|
454
|
+
const pathWithExtension = checkFileExtensionsUntilItExists(`${pathToDependencyFromDirectory}/index`);
|
|
455
|
+
if (pathWithExtension) pathToDependencyFromDirectory = pathWithExtension;
|
|
456
|
+
else console.warn(`Could not find index file for directory at ${pathToDependencyFromDirectory}. This is probably going to cause issues with both hot reloading and your code.`);
|
|
457
|
+
}
|
|
458
|
+
const extension = path.extname(pathToDependencyFromDirectory);
|
|
459
|
+
const pathWithEnsuredExtension = (() => {
|
|
460
|
+
if (extension.length > 0 && existsSync(pathToDependencyFromDirectory)) return pathToDependencyFromDirectory;
|
|
461
|
+
if (javascriptExtensions.includes(extension)) return checkFileExtensionsUntilItExists(pathToDependencyFromDirectory.replace(extension, ""));
|
|
462
|
+
return checkFileExtensionsUntilItExists(pathToDependencyFromDirectory);
|
|
463
|
+
})();
|
|
464
|
+
if (pathWithEnsuredExtension) pathToDependencyFromDirectory = pathWithEnsuredExtension;
|
|
465
|
+
else console.warn(`Could not find file at ${pathToDependencyFromDirectory}`);
|
|
466
|
+
return pathToDependencyFromDirectory;
|
|
467
|
+
});
|
|
468
|
+
const moduleDependencies = importedPathsRelativeToDirectory.filter((dependencyPath) => !dependencyPath.startsWith(".") && !path.isAbsolute(dependencyPath));
|
|
469
|
+
return {
|
|
470
|
+
dependencyPaths: importedPathsRelativeToDirectory.filter((dependencyPath) => dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)),
|
|
471
|
+
moduleDependencies
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
const updateModuleDependenciesInGraph = async (moduleFilePath) => {
|
|
475
|
+
if (graph[moduleFilePath] === void 0) graph[moduleFilePath] = {
|
|
476
|
+
path: moduleFilePath,
|
|
477
|
+
dependencyPaths: [],
|
|
478
|
+
dependentPaths: [],
|
|
479
|
+
moduleDependencies: []
|
|
480
|
+
};
|
|
481
|
+
const { moduleDependencies, dependencyPaths: newDependencyPaths } = await getDependencyPaths(moduleFilePath);
|
|
482
|
+
graph[moduleFilePath].moduleDependencies = moduleDependencies;
|
|
483
|
+
for (const dependencyPath of graph[moduleFilePath].dependencyPaths) {
|
|
484
|
+
if (newDependencyPaths.includes(dependencyPath)) continue;
|
|
485
|
+
const dependencyModule = graph[dependencyPath];
|
|
486
|
+
if (dependencyModule !== void 0) dependencyModule.dependentPaths = dependencyModule.dependentPaths.filter((dependentPath) => dependentPath !== moduleFilePath);
|
|
487
|
+
}
|
|
488
|
+
graph[moduleFilePath].dependencyPaths = newDependencyPaths;
|
|
489
|
+
for (const dependencyPath of newDependencyPaths) {
|
|
490
|
+
if (graph[dependencyPath] === void 0) await updateModuleDependenciesInGraph(dependencyPath);
|
|
491
|
+
const dependencyModule = graph[dependencyPath];
|
|
492
|
+
if (dependencyModule === void 0) throw new Error(`Loading the dependency path ${dependencyPath} did not initialize it at all. This is a bug in React PDF.`);
|
|
493
|
+
if (!dependencyModule.dependentPaths.includes(moduleFilePath)) dependencyModule.dependentPaths.push(moduleFilePath);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
for (const filePath of modulePaths) await updateModuleDependenciesInGraph(filePath);
|
|
497
|
+
const removeModuleFromGraph = (filePath) => {
|
|
498
|
+
const module = graph[filePath];
|
|
499
|
+
if (module) {
|
|
500
|
+
for (const dependencyPath of module.dependencyPaths) if (graph[dependencyPath]) graph[dependencyPath].dependentPaths = graph[dependencyPath].dependentPaths.filter((dependentPath) => dependentPath !== filePath);
|
|
501
|
+
delete graph[filePath];
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
return [
|
|
505
|
+
graph,
|
|
506
|
+
async (event, pathToModified) => {
|
|
507
|
+
switch (event) {
|
|
508
|
+
case "change":
|
|
509
|
+
if (isJavascriptModule(pathToModified)) await updateModuleDependenciesInGraph(pathToModified);
|
|
510
|
+
break;
|
|
511
|
+
case "add":
|
|
512
|
+
if (isJavascriptModule(pathToModified)) await updateModuleDependenciesInGraph(pathToModified);
|
|
513
|
+
break;
|
|
514
|
+
case "addDir": {
|
|
515
|
+
const modulesInsideAddedDirectory = (await readAllFilesInsideDirectory(pathToModified)).filter(isJavascriptModule);
|
|
516
|
+
for (const filePath of modulesInsideAddedDirectory) await updateModuleDependenciesInGraph(filePath);
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "unlink":
|
|
520
|
+
if (isJavascriptModule(pathToModified)) removeModuleFromGraph(pathToModified);
|
|
521
|
+
break;
|
|
522
|
+
case "unlinkDir": {
|
|
523
|
+
const modulesInsideDeletedDirectory = (await readAllFilesInsideDirectory(pathToModified)).filter(isJavascriptModule);
|
|
524
|
+
for (const filePath of modulesInsideDeletedDirectory) removeModuleFromGraph(filePath);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
{ resolveDependentsOf: function resolveDependentsOf(pathToModule) {
|
|
530
|
+
const dependentPaths = /* @__PURE__ */ new Set();
|
|
531
|
+
const stack = [pathToModule];
|
|
532
|
+
while (stack.length > 0) {
|
|
533
|
+
const moduleEntry = graph[stack.pop()];
|
|
534
|
+
if (!moduleEntry) continue;
|
|
535
|
+
for (const dependentPath of moduleEntry.dependentPaths) {
|
|
536
|
+
if (dependentPaths.has(dependentPath) || dependentPath === pathToModule) continue;
|
|
537
|
+
dependentPaths.add(dependentPath);
|
|
538
|
+
stack.push(dependentPath);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return [...dependentPaths.values()];
|
|
542
|
+
} }
|
|
543
|
+
];
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/utils/preview/hot-reloading/setup-hot-reloading.ts
|
|
548
|
+
const setupHotreloading = async (devServer$1, templatesDirRelativePath) => {
|
|
549
|
+
let clients = [];
|
|
550
|
+
new Server(devServer$1).on("connection", (client) => {
|
|
551
|
+
clients.push(client);
|
|
552
|
+
client.on("disconnect", () => {
|
|
553
|
+
clients = clients.filter((item) => item !== client);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
let changes = [];
|
|
557
|
+
const reload = debounce(() => {
|
|
558
|
+
clients.forEach((client) => {
|
|
559
|
+
client.emit("reload", changes.filter((change) => path.resolve(absolutePathToTemplatesDirectory, change.filename).startsWith(absolutePathToTemplatesDirectory)));
|
|
560
|
+
});
|
|
561
|
+
changes = [];
|
|
562
|
+
}, 150);
|
|
563
|
+
const absolutePathToTemplatesDirectory = path.resolve(process.cwd(), templatesDirRelativePath);
|
|
564
|
+
const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] = await createDependencyGraph(absolutePathToTemplatesDirectory);
|
|
565
|
+
const watcher = watch("", {
|
|
566
|
+
ignoreInitial: true,
|
|
567
|
+
cwd: absolutePathToTemplatesDirectory
|
|
568
|
+
});
|
|
569
|
+
const getFilesOutsideTemplatesDirectory = () => Object.keys(dependencyGraph).filter((p) => path.relative(absolutePathToTemplatesDirectory, p).startsWith(".."));
|
|
570
|
+
let filesOutsideTemplatesDirectory = getFilesOutsideTemplatesDirectory();
|
|
571
|
+
for (const p of filesOutsideTemplatesDirectory) watcher.add(p);
|
|
572
|
+
const exit = async () => {
|
|
573
|
+
await watcher.close();
|
|
574
|
+
};
|
|
575
|
+
process.on("SIGINT", exit);
|
|
576
|
+
process.on("uncaughtException", exit);
|
|
577
|
+
watcher.on("all", async (event, relativePathToChangeTarget) => {
|
|
578
|
+
if (relativePathToChangeTarget.split(path.sep).length === 0) return;
|
|
579
|
+
const pathToChangeTarget = path.resolve(absolutePathToTemplatesDirectory, relativePathToChangeTarget);
|
|
580
|
+
await updateDependencyGraph(event, pathToChangeTarget);
|
|
581
|
+
const newFilesOutsideTemplatesDirectory = getFilesOutsideTemplatesDirectory();
|
|
582
|
+
for (const p of filesOutsideTemplatesDirectory) if (!newFilesOutsideTemplatesDirectory.includes(p)) watcher.unwatch(p);
|
|
583
|
+
for (const p of newFilesOutsideTemplatesDirectory) if (!filesOutsideTemplatesDirectory.includes(p)) watcher.add(p);
|
|
584
|
+
filesOutsideTemplatesDirectory = newFilesOutsideTemplatesDirectory;
|
|
585
|
+
changes.push({
|
|
586
|
+
event,
|
|
587
|
+
filename: relativePathToChangeTarget
|
|
588
|
+
});
|
|
589
|
+
for (const dependentPath of resolveDependentsOf(pathToChangeTarget)) changes.push({
|
|
590
|
+
event: "change",
|
|
591
|
+
filename: path.relative(absolutePathToTemplatesDirectory, dependentPath)
|
|
592
|
+
});
|
|
593
|
+
reload();
|
|
594
|
+
});
|
|
595
|
+
return watcher;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/utils/style-text.ts
|
|
600
|
+
const styleText = nodeUtil.styleText ? nodeUtil.styleText : (_, text) => text;
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/utils/preview/get-env-variables-for-preview-app.ts
|
|
604
|
+
const getEnvVariablesForPreviewApp = (relativePathToTemplatesDirectory, previewServerLocation, cwd) => {
|
|
605
|
+
return {
|
|
606
|
+
TEMPLATES_DIR_RELATIVE_PATH: relativePathToTemplatesDirectory,
|
|
607
|
+
TEMPLATES_DIR_ABSOLUTE_PATH: path.resolve(cwd, relativePathToTemplatesDirectory),
|
|
608
|
+
PREVIEW_SERVER_LOCATION: previewServerLocation,
|
|
609
|
+
USER_PROJECT_LOCATION: cwd
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/utils/preview/serve-static-file.ts
|
|
615
|
+
/**
|
|
616
|
+
* Extracts the template directory path from the referer URL.
|
|
617
|
+
* @param referer - The referer header value
|
|
618
|
+
* @returns The template directory path, or null if not found
|
|
619
|
+
*/
|
|
620
|
+
const extractTemplatePathFromReferer = (referer) => {
|
|
621
|
+
try {
|
|
622
|
+
const previewMatch = new URL(referer).pathname.match(/\/preview\/(.+)$/);
|
|
623
|
+
if (!previewMatch?.[1]) return null;
|
|
624
|
+
return previewMatch[1].replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
625
|
+
} catch {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* Recursively searches for a static file starting from the template directory
|
|
631
|
+
* and traversing up to the templates root directory.
|
|
632
|
+
* @param templateFullPath - Full path to the template directory
|
|
633
|
+
* @param templatesDirResolved - Resolved path to the templates root directory
|
|
634
|
+
* @param relativeFilePath - Relative path to the file within the static folder
|
|
635
|
+
* @returns Absolute path to the found file, or null if not found
|
|
636
|
+
*/
|
|
637
|
+
const findStaticFileRecursively = (templateFullPath, templatesDirResolved, relativeFilePath) => {
|
|
638
|
+
let currentDir = templateFullPath;
|
|
639
|
+
while (currentDir.startsWith(templatesDirResolved)) {
|
|
640
|
+
const staticPath = path.join(currentDir, "static", relativeFilePath);
|
|
641
|
+
if (existsSync(staticPath)) return staticPath;
|
|
642
|
+
const parentDir = path.dirname(currentDir);
|
|
643
|
+
if (parentDir === currentDir) break;
|
|
644
|
+
currentDir = parentDir;
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
};
|
|
648
|
+
const serveStaticFile = async (res, parsedUrl, staticDirRelativePath, templatesDirRelativePath, req) => {
|
|
649
|
+
const originalPath = parsedUrl.pathname;
|
|
650
|
+
const pathname = originalPath.startsWith("/static") ? originalPath.replace("/static", "./static") : `./static${originalPath}`;
|
|
651
|
+
const ext = path.parse(pathname).ext;
|
|
652
|
+
const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath);
|
|
653
|
+
let fileAbsolutePath = null;
|
|
654
|
+
if (templatesDirRelativePath && req?.headers.referer) {
|
|
655
|
+
const templateDirPath = extractTemplatePathFromReferer(req.headers.referer);
|
|
656
|
+
if (templateDirPath) {
|
|
657
|
+
const templatesDir = path.resolve(process.cwd(), templatesDirRelativePath);
|
|
658
|
+
const templateFullPath = path.join(templatesDir, templateDirPath);
|
|
659
|
+
const relativeFilePath = pathname.replace("./static/", "");
|
|
660
|
+
fileAbsolutePath = findStaticFileRecursively(templateFullPath, path.resolve(templatesDir), relativeFilePath);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!fileAbsolutePath) {
|
|
664
|
+
fileAbsolutePath = path.resolve(staticBaseDir, pathname);
|
|
665
|
+
if (!fileAbsolutePath.startsWith(staticBaseDir)) {
|
|
666
|
+
res.statusCode = 403;
|
|
667
|
+
res.end();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const fileHandle = await promises.open(fileAbsolutePath, "r");
|
|
673
|
+
const fileData = await promises.readFile(fileHandle);
|
|
674
|
+
res.setHeader("Content-type", lookup(ext) || "text/plain");
|
|
675
|
+
res.end(fileData);
|
|
676
|
+
fileHandle.close();
|
|
677
|
+
} catch (exception) {
|
|
678
|
+
if (!existsSync(fileAbsolutePath)) {
|
|
679
|
+
res.statusCode = 404;
|
|
680
|
+
res.end();
|
|
681
|
+
} else {
|
|
682
|
+
const sanitizedFilePath = fileAbsolutePath.replace(/\n|\r/g, "");
|
|
683
|
+
console.error(`Could not read file at %s to be served, here's the exception:`, sanitizedFilePath, exception);
|
|
684
|
+
res.statusCode = 500;
|
|
685
|
+
res.end("Could not read file to be served! Check your terminal for more information.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/utils/preview/start-dev-server.ts
|
|
692
|
+
let devServer;
|
|
693
|
+
const safeAsyncServerListen = (server, port) => {
|
|
694
|
+
return new Promise((resolve) => {
|
|
695
|
+
server.listen(port, () => {
|
|
696
|
+
resolve({ portAlreadyInUse: false });
|
|
697
|
+
});
|
|
698
|
+
server.on("error", (e) => {
|
|
699
|
+
if (e.code === "EADDRINUSE") resolve({ portAlreadyInUse: true });
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
const startDevServer = async (templatesDirRelativePath, staticBaseDirRelativePath, port) => {
|
|
704
|
+
const [majorNodeVersion] = process.versions.node.split(".");
|
|
705
|
+
if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) {
|
|
706
|
+
console.error(` ${logSymbols.error} Node ${majorNodeVersion} is not supported. Please upgrade to Node 20 or higher.`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const previewServerLocation = await getPreviewServerLocation();
|
|
710
|
+
const previewServer = createJiti(previewServerLocation);
|
|
711
|
+
devServer = http.createServer((req, res) => {
|
|
712
|
+
if (!req.url) {
|
|
713
|
+
res.end(404);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const parsedUrl = url.parse(req.url, true);
|
|
717
|
+
res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store");
|
|
718
|
+
res.setHeader("Pragma", "no-cache");
|
|
719
|
+
res.setHeader("Expires", "-1");
|
|
720
|
+
try {
|
|
721
|
+
if (parsedUrl.path && !parsedUrl.path.startsWith("/preview/") && !parsedUrl.path.startsWith("/api/") && !parsedUrl.path.includes("_next/")) serveStaticFile(res, parsedUrl, staticBaseDirRelativePath, templatesDirRelativePath, req);
|
|
722
|
+
else if (!isNextReady) nextReadyPromise.then(() => nextHandleRequest?.(req, res, parsedUrl));
|
|
723
|
+
else nextHandleRequest?.(req, res, parsedUrl);
|
|
724
|
+
} catch (e) {
|
|
725
|
+
console.error("caught error", e);
|
|
726
|
+
res.writeHead(500);
|
|
727
|
+
res.end();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const { portAlreadyInUse } = await safeAsyncServerListen(devServer, port);
|
|
731
|
+
if (!portAlreadyInUse) {
|
|
732
|
+
console.log(styleText("greenBright", ` React PDF ${package_default.version}`));
|
|
733
|
+
console.log(` Running preview at: http://localhost:${port}\n`);
|
|
734
|
+
} else {
|
|
735
|
+
const nextPortToTry = port + 1;
|
|
736
|
+
console.warn(` ${logSymbols.warning} Port ${port} is already in use, trying ${nextPortToTry}`);
|
|
737
|
+
return startDevServer(templatesDirRelativePath, staticBaseDirRelativePath, nextPortToTry);
|
|
738
|
+
}
|
|
739
|
+
devServer.on("close", () => {
|
|
740
|
+
app.close();
|
|
741
|
+
});
|
|
742
|
+
devServer.on("error", (e) => {
|
|
743
|
+
spinner.stopAndPersist({
|
|
744
|
+
symbol: logSymbols.error,
|
|
745
|
+
text: `Preview Server had an error: ${e.message}`
|
|
746
|
+
});
|
|
747
|
+
process.exit(1);
|
|
748
|
+
});
|
|
749
|
+
const spinner = ora({
|
|
750
|
+
text: "Getting react-pdf preview server ready...\n",
|
|
751
|
+
prefixText: " "
|
|
752
|
+
}).start();
|
|
753
|
+
registerSpinnerAutostopping(spinner);
|
|
754
|
+
const timeBeforeNextReady = performance.now();
|
|
755
|
+
process.env = {
|
|
756
|
+
NODE_ENV: "development",
|
|
757
|
+
...process.env,
|
|
758
|
+
...getEnvVariablesForPreviewApp(path.normalize(templatesDirRelativePath), previewServerLocation, process.cwd())
|
|
759
|
+
};
|
|
760
|
+
const app = (await previewServer.import("next", { default: true }))({
|
|
761
|
+
dev: false,
|
|
762
|
+
conf: { images: { unoptimized: true } },
|
|
763
|
+
hostname: "localhost",
|
|
764
|
+
port,
|
|
765
|
+
dir: previewServerLocation
|
|
766
|
+
});
|
|
767
|
+
let isNextReady = false;
|
|
768
|
+
const nextReadyPromise = app.prepare();
|
|
769
|
+
try {
|
|
770
|
+
await nextReadyPromise;
|
|
771
|
+
} catch (exception) {
|
|
772
|
+
spinner.stopAndPersist({
|
|
773
|
+
symbol: logSymbols.error,
|
|
774
|
+
text: ` Preview Server had an error: ${exception}`
|
|
775
|
+
});
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
isNextReady = true;
|
|
779
|
+
const nextHandleRequest = app.getRequestHandler();
|
|
780
|
+
const secondsToNextReady = ((performance.now() - timeBeforeNextReady) / 1e3).toFixed(1);
|
|
781
|
+
spinner.stopAndPersist({
|
|
782
|
+
text: `Ready in ${secondsToNextReady}s\n`,
|
|
783
|
+
symbol: logSymbols.success
|
|
784
|
+
});
|
|
785
|
+
return devServer;
|
|
786
|
+
};
|
|
787
|
+
const makeExitHandler = (options) => (codeSignalOrError) => {
|
|
788
|
+
if (typeof devServer !== "undefined") {
|
|
789
|
+
console.log("\nshutting down dev server");
|
|
790
|
+
devServer.close();
|
|
791
|
+
devServer = void 0;
|
|
792
|
+
}
|
|
793
|
+
if (codeSignalOrError instanceof Error) console.error(codeSignalOrError);
|
|
794
|
+
if (options?.shouldKillProcess) process.exit(options.killWithErrorCode ? 1 : 0);
|
|
795
|
+
};
|
|
796
|
+
process.on("exit", makeExitHandler());
|
|
797
|
+
process.on("SIGINT", makeExitHandler({
|
|
798
|
+
shouldKillProcess: true,
|
|
799
|
+
killWithErrorCode: false
|
|
800
|
+
}));
|
|
801
|
+
process.on("SIGUSR1", makeExitHandler({
|
|
802
|
+
shouldKillProcess: true,
|
|
803
|
+
killWithErrorCode: false
|
|
804
|
+
}));
|
|
805
|
+
process.on("SIGUSR2", makeExitHandler({
|
|
806
|
+
shouldKillProcess: true,
|
|
807
|
+
killWithErrorCode: false
|
|
808
|
+
}));
|
|
809
|
+
process.on("uncaughtException", makeExitHandler({
|
|
810
|
+
shouldKillProcess: true,
|
|
811
|
+
killWithErrorCode: true
|
|
812
|
+
}));
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/utils/tree.ts
|
|
816
|
+
const SYMBOLS = {
|
|
817
|
+
BRANCH: "├── ",
|
|
818
|
+
EMPTY: "",
|
|
819
|
+
INDENT: " ",
|
|
820
|
+
LAST_BRANCH: "└── ",
|
|
821
|
+
VERTICAL: "│ "
|
|
822
|
+
};
|
|
823
|
+
const getTreeLines = async (dirPath, depth, currentDepth = 0) => {
|
|
824
|
+
const base = process.cwd();
|
|
825
|
+
const dirFullpath = path.resolve(base, dirPath);
|
|
826
|
+
let lines = [path.basename(dirFullpath)];
|
|
827
|
+
if ((await promises.stat(dirFullpath)).isDirectory() && currentDepth < depth) {
|
|
828
|
+
const childDirents = await promises.readdir(dirFullpath, { withFileTypes: true });
|
|
829
|
+
childDirents.sort((a, b) => {
|
|
830
|
+
if (a.isDirectory() && b.isFile()) return -1;
|
|
831
|
+
if (a.isFile() && b.isDirectory()) return 1;
|
|
832
|
+
return b.name > a.name ? -1 : 1;
|
|
833
|
+
});
|
|
834
|
+
for (let i = 0; i < childDirents.length; i++) {
|
|
835
|
+
const dirent = childDirents[i];
|
|
836
|
+
const isLast = i === childDirents.length - 1;
|
|
837
|
+
const branchingSymbol = isLast ? SYMBOLS.LAST_BRANCH : SYMBOLS.BRANCH;
|
|
838
|
+
const verticalSymbol = isLast ? SYMBOLS.INDENT : SYMBOLS.VERTICAL;
|
|
839
|
+
if (dirent.isFile()) lines.push(`${branchingSymbol}${dirent.name}`);
|
|
840
|
+
else {
|
|
841
|
+
const treeLinesForSubDirectory = await getTreeLines(path.join(dirFullpath, dirent.name), depth, currentDepth + 1);
|
|
842
|
+
lines = lines.concat(treeLinesForSubDirectory.map((line, index) => index === 0 ? `${branchingSymbol}${line}` : `${verticalSymbol}${line}`));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return lines;
|
|
847
|
+
};
|
|
848
|
+
const tree = async (dirPath, depth) => {
|
|
849
|
+
return (await getTreeLines(dirPath, depth)).join(os.EOL);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/commands/dev.ts
|
|
854
|
+
const dev = async ({ dir: templatesDirRelativePath, port }) => {
|
|
855
|
+
try {
|
|
856
|
+
if (!fs.existsSync(templatesDirRelativePath)) {
|
|
857
|
+
console.error(`Missing ${templatesDirRelativePath} folder`);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
await setupHotreloading(await startDevServer(templatesDirRelativePath, templatesDirRelativePath, Number.parseInt(port, 10)), templatesDirRelativePath);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
console.log(error);
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/utils/esbuild/escape-string-for-regex.ts
|
|
869
|
+
function escapeStringForRegex(string) {
|
|
870
|
+
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/utils/esbuild/renderring-utilities-exporter.ts
|
|
875
|
+
/**
|
|
876
|
+
* Made to export the `render` function out of the user's PDF template
|
|
877
|
+
* so that React version mismatches don't happen.
|
|
878
|
+
*
|
|
879
|
+
* This also exports the `createElement` from the user's React version as well
|
|
880
|
+
* to avoid mismatches.
|
|
881
|
+
*
|
|
882
|
+
* This avoids multiple versions of React being involved, i.e., the version
|
|
883
|
+
* in the CLI vs. the version the user has on their templates.
|
|
884
|
+
*/
|
|
885
|
+
const renderingUtilitiesExporter = (pdfTemplates) => ({
|
|
886
|
+
name: "rendering-utilities-exporter",
|
|
887
|
+
setup: (b) => {
|
|
888
|
+
b.onLoad({ filter: new RegExp(pdfTemplates.map((templatePath) => escapeStringForRegex(templatePath)).join("|")) }, async ({ path: pathToFile }) => {
|
|
889
|
+
return {
|
|
890
|
+
contents: `${await promises.readFile(pathToFile, "utf8")};
|
|
891
|
+
export { render } from 'react-pdf-module-that-will-export-render'
|
|
892
|
+
export { createElement as reactPDFCreateReactElement } from 'react';
|
|
893
|
+
`,
|
|
894
|
+
loader: path.extname(pathToFile).slice(1)
|
|
895
|
+
};
|
|
896
|
+
});
|
|
897
|
+
b.onResolve({ filter: /^react-pdf-module-that-will-export-render$/ }, async (args) => {
|
|
898
|
+
const options = {
|
|
899
|
+
kind: "import-statement",
|
|
900
|
+
importer: args.importer,
|
|
901
|
+
resolveDir: args.resolveDir,
|
|
902
|
+
namespace: args.namespace
|
|
903
|
+
};
|
|
904
|
+
let result = await b.resolve("@ahmedrowaihi/pdf-forge-core", options);
|
|
905
|
+
if (result.errors.length === 0) return result;
|
|
906
|
+
result = await b.resolve("@ahmedrowaihi/pdf-forge-components", options);
|
|
907
|
+
if (result.errors.length > 0 && result.errors[0]) result.errors[0].text = "Failed trying to import `render` from either `@ahmedrowaihi/pdf-forge-core` or `@ahmedrowaihi/pdf-forge-components` to be able to render your PDF template.\n Maybe you don't have either of them installed?";
|
|
908
|
+
return result;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/commands/export.ts
|
|
915
|
+
const getTemplatesFromDirectory = (templateDirectory) => {
|
|
916
|
+
const templatePaths = [];
|
|
917
|
+
for (const filename of templateDirectory.templateFilenames) templatePaths.push(path.join(templateDirectory.absolutePath, filename));
|
|
918
|
+
for (const directory of templateDirectory.subDirectories) templatePaths.push(...getTemplatesFromDirectory(directory));
|
|
919
|
+
return templatePaths;
|
|
920
|
+
};
|
|
921
|
+
const require = createRequire(url.fileURLToPath(import.meta.url));
|
|
922
|
+
const exportTemplates = async (pathToWhereTemplateMarkupShouldBeDumped, templatesDirectoryPath, options) => {
|
|
923
|
+
if (fs.existsSync(pathToWhereTemplateMarkupShouldBeDumped)) fs.rmSync(pathToWhereTemplateMarkupShouldBeDumped, { recursive: true });
|
|
924
|
+
let spinner;
|
|
925
|
+
if (!options.silent) {
|
|
926
|
+
spinner = ora("Preparing files...\n").start();
|
|
927
|
+
registerSpinnerAutostopping(spinner);
|
|
928
|
+
}
|
|
929
|
+
const templatesDirectoryMetadata = await getTemplatesDirectoryMetadata(path.resolve(process.cwd(), templatesDirectoryPath), true);
|
|
930
|
+
if (typeof templatesDirectoryMetadata === "undefined") {
|
|
931
|
+
if (spinner) spinner.stopAndPersist({
|
|
932
|
+
symbol: logSymbols.error,
|
|
933
|
+
text: `Could not find the directory at ${templatesDirectoryPath}`
|
|
934
|
+
});
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const allTemplates = getTemplatesFromDirectory(templatesDirectoryMetadata);
|
|
938
|
+
try {
|
|
939
|
+
await build({
|
|
940
|
+
bundle: true,
|
|
941
|
+
entryPoints: allTemplates,
|
|
942
|
+
format: "cjs",
|
|
943
|
+
jsx: "automatic",
|
|
944
|
+
loader: { ".js": "jsx" },
|
|
945
|
+
logLevel: "silent",
|
|
946
|
+
outExtension: { ".js": ".cjs" },
|
|
947
|
+
outdir: pathToWhereTemplateMarkupShouldBeDumped,
|
|
948
|
+
platform: "node",
|
|
949
|
+
plugins: [renderingUtilitiesExporter(allTemplates)],
|
|
950
|
+
write: true
|
|
951
|
+
});
|
|
952
|
+
} catch (exception) {
|
|
953
|
+
if (spinner) spinner.stopAndPersist({
|
|
954
|
+
symbol: logSymbols.error,
|
|
955
|
+
text: "Failed to build PDF templates"
|
|
956
|
+
});
|
|
957
|
+
const buildFailure = exception;
|
|
958
|
+
console.error(`\n${buildFailure.message}`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
if (spinner) spinner.succeed();
|
|
962
|
+
const allBuiltTemplates = glob.sync(normalize(`${pathToWhereTemplateMarkupShouldBeDumped}/**/*.cjs`), { absolute: true });
|
|
963
|
+
for await (const template of allBuiltTemplates) try {
|
|
964
|
+
if (spinner) {
|
|
965
|
+
spinner.text = `rendering ${template.split("/").pop()}`;
|
|
966
|
+
spinner.render();
|
|
967
|
+
}
|
|
968
|
+
delete require.cache[template];
|
|
969
|
+
const templateModule = require(template);
|
|
970
|
+
const rendered = await templateModule.render(templateModule.reactPDFCreateReactElement(templateModule.default, {}), options);
|
|
971
|
+
writeFileSync(template.replace(".cjs", options.plainText ? ".txt" : ".html"), rendered);
|
|
972
|
+
unlinkSync(template);
|
|
973
|
+
} catch (exception) {
|
|
974
|
+
if (spinner) spinner.stopAndPersist({
|
|
975
|
+
symbol: logSymbols.error,
|
|
976
|
+
text: `failed when rendering ${template.split("/").pop()}`
|
|
977
|
+
});
|
|
978
|
+
console.error(exception);
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
if (spinner) {
|
|
982
|
+
spinner.succeed("Rendered all files");
|
|
983
|
+
spinner.text = "Copying static files";
|
|
984
|
+
spinner.render();
|
|
985
|
+
}
|
|
986
|
+
const staticDirectoryPath = path.join(templatesDirectoryPath, "static");
|
|
987
|
+
if (fs.existsSync(staticDirectoryPath)) {
|
|
988
|
+
const pathToDumpStaticFilesInto = path.join(pathToWhereTemplateMarkupShouldBeDumped, "static");
|
|
989
|
+
if (fs.existsSync(pathToDumpStaticFilesInto)) await fs.promises.rm(pathToDumpStaticFilesInto, { recursive: true });
|
|
990
|
+
try {
|
|
991
|
+
await fs.promises.cp(staticDirectoryPath, pathToDumpStaticFilesInto, { recursive: true });
|
|
992
|
+
} catch (exception) {
|
|
993
|
+
console.error(exception);
|
|
994
|
+
if (spinner) spinner.stopAndPersist({
|
|
995
|
+
symbol: logSymbols.error,
|
|
996
|
+
text: "Failed to copy static files"
|
|
997
|
+
});
|
|
998
|
+
console.error(`Something went wrong while copying the file to ${pathToWhereTemplateMarkupShouldBeDumped}/static, ${exception}`);
|
|
999
|
+
process.exit(1);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (spinner && !options.silent) {
|
|
1003
|
+
spinner.succeed();
|
|
1004
|
+
const fileTree = await tree(pathToWhereTemplateMarkupShouldBeDumped, 4);
|
|
1005
|
+
console.log(fileTree);
|
|
1006
|
+
spinner.stopAndPersist({
|
|
1007
|
+
symbol: logSymbols.success,
|
|
1008
|
+
text: "Successfully exported PDF templates"
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
//#endregion
|
|
1014
|
+
//#region src/commands/start.ts
|
|
1015
|
+
const start = async () => {
|
|
1016
|
+
try {
|
|
1017
|
+
const previewServerLocation = await getPreviewServerLocation();
|
|
1018
|
+
const usersProjectLocation = process.cwd();
|
|
1019
|
+
const builtPreviewPath = path.resolve(usersProjectLocation, "./.react-pdf");
|
|
1020
|
+
if (!fs.existsSync(builtPreviewPath)) {
|
|
1021
|
+
console.error("Could not find .react-pdf, maybe you haven't ran pdf-dev build?");
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
const nextStart = spawn("npx", [
|
|
1025
|
+
"next",
|
|
1026
|
+
"start",
|
|
1027
|
+
builtPreviewPath
|
|
1028
|
+
], {
|
|
1029
|
+
cwd: previewServerLocation,
|
|
1030
|
+
stdio: "inherit"
|
|
1031
|
+
});
|
|
1032
|
+
process.on("SIGINT", () => {
|
|
1033
|
+
nextStart.kill("SIGINT");
|
|
1034
|
+
});
|
|
1035
|
+
nextStart.on("exit", (code) => {
|
|
1036
|
+
process.exit(code ?? 0);
|
|
1037
|
+
});
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
console.log(error);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/index.ts
|
|
1046
|
+
program.name("react-pdf").description("A live preview of your PDF templates right in your browser").version(package_default.version);
|
|
1047
|
+
program.command("dev").description("Starts the preview PDF development app").option("-d, --dir <path>", "Directory with your PDF templates", "./templates").option("-p --port <port>", "Port to run dev server on", "3000").action(dev);
|
|
1048
|
+
program.command("build").description("Copies the preview app for onto .react-pdf and builds it").option("-d, --dir <path>", "Directory with your PDF templates", "./templates").option("-p --packageManager <name>", "Package name to use on installation on `.react-pdf`", "npm").action(build$1);
|
|
1049
|
+
program.command("start").description("Runs the built preview app that is inside of \".react-pdf\"").action(start);
|
|
1050
|
+
program.command("export").description("Build the templates to the `out` directory").option("--outDir <path>", "Output directory", "out").option("-p, --pretty", "Pretty print the output", false).option("-t, --plainText", "Set output format as plain text", false).option("-d, --dir <path>", "Directory with your PDF templates", "./templates").option("-s, --silent", "To, or not to show a spinner with process information", false).action(({ outDir, pretty, plainText, silent, dir: srcDir }) => exportTemplates(outDir, srcDir, {
|
|
1051
|
+
silent,
|
|
1052
|
+
plainText,
|
|
1053
|
+
pretty
|
|
1054
|
+
}));
|
|
1055
|
+
program.parse();
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
export { };
|