@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
|
|
3
|
+
import traverseModule from '@babel/traverse';
|
|
4
|
+
|
|
5
|
+
const traverse =
|
|
6
|
+
// we keep this check here so that this still works with the dev:preview
|
|
7
|
+
// script's use of tsx
|
|
8
|
+
typeof traverseModule === 'function'
|
|
9
|
+
? traverseModule
|
|
10
|
+
: traverseModule.default;
|
|
11
|
+
|
|
12
|
+
export const getImportedModules = (contents: string) => {
|
|
13
|
+
const importedPaths: string[] = [];
|
|
14
|
+
const parsedContents = parse(contents, {
|
|
15
|
+
sourceType: 'unambiguous',
|
|
16
|
+
strictMode: false,
|
|
17
|
+
errorRecovery: true,
|
|
18
|
+
plugins: ['jsx', 'typescript', 'decorators'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
traverse(parsedContents, {
|
|
22
|
+
ImportDeclaration({ node }) {
|
|
23
|
+
importedPaths.push(node.source.value);
|
|
24
|
+
},
|
|
25
|
+
ExportAllDeclaration({ node }) {
|
|
26
|
+
importedPaths.push(node.source.value);
|
|
27
|
+
},
|
|
28
|
+
ExportNamedDeclaration({ node }) {
|
|
29
|
+
if (node.source) {
|
|
30
|
+
importedPaths.push(node.source.value);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
TSExternalModuleReference({ node }) {
|
|
34
|
+
importedPaths.push(node.expression.value);
|
|
35
|
+
},
|
|
36
|
+
CallExpression({ node }) {
|
|
37
|
+
if ('name' in node.callee && node.callee.name === 'require') {
|
|
38
|
+
if (node.arguments.length === 1) {
|
|
39
|
+
const importPathNode = node.arguments[0]!;
|
|
40
|
+
if (importPathNode.type === 'StringLiteral') {
|
|
41
|
+
importedPaths.push(importPathNode.value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return importedPaths;
|
|
49
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createMatchPath, loadConfig } from 'tsconfig-paths';
|
|
3
|
+
|
|
4
|
+
export const resolvePathAliases = (
|
|
5
|
+
importPaths: string[],
|
|
6
|
+
projectPath: string,
|
|
7
|
+
) => {
|
|
8
|
+
const configLoadResult = loadConfig(projectPath);
|
|
9
|
+
|
|
10
|
+
if (configLoadResult.resultType === 'success') {
|
|
11
|
+
const matchPath = createMatchPath(
|
|
12
|
+
configLoadResult.absoluteBaseUrl,
|
|
13
|
+
configLoadResult.paths,
|
|
14
|
+
);
|
|
15
|
+
return importPaths.map((importedPath) => {
|
|
16
|
+
const unaliasedPath = matchPath(importedPath, undefined, undefined, [
|
|
17
|
+
'.tsx',
|
|
18
|
+
'.ts',
|
|
19
|
+
'.js',
|
|
20
|
+
'.jsx',
|
|
21
|
+
'.cjs',
|
|
22
|
+
'.mjs',
|
|
23
|
+
]);
|
|
24
|
+
if (unaliasedPath) {
|
|
25
|
+
return `./${path.relative(projectPath, unaliasedPath)}`;
|
|
26
|
+
}
|
|
27
|
+
return importedPath;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return importPaths;
|
|
32
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type http from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { watch } from 'chokidar';
|
|
4
|
+
import debounce from 'debounce';
|
|
5
|
+
import { type Socket, Server as SocketServer } from 'socket.io';
|
|
6
|
+
import type { HotReloadChange } from '../../types/hot-reload-change.js';
|
|
7
|
+
import { createDependencyGraph } from './create-dependency-graph.js';
|
|
8
|
+
|
|
9
|
+
export const setupHotreloading = async (
|
|
10
|
+
devServer: http.Server,
|
|
11
|
+
templatesDirRelativePath: string,
|
|
12
|
+
) => {
|
|
13
|
+
let clients: Socket[] = [];
|
|
14
|
+
const io = new SocketServer(devServer);
|
|
15
|
+
|
|
16
|
+
io.on('connection', (client) => {
|
|
17
|
+
clients.push(client);
|
|
18
|
+
|
|
19
|
+
client.on('disconnect', () => {
|
|
20
|
+
clients = clients.filter((item) => item !== client);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// used to keep track of all changes
|
|
25
|
+
// and send them at once to the preview app through the web socket
|
|
26
|
+
let changes = [] as HotReloadChange[];
|
|
27
|
+
|
|
28
|
+
const reload = debounce(() => {
|
|
29
|
+
// we detect these using the useHotreload hook on the Next app
|
|
30
|
+
clients.forEach((client) => {
|
|
31
|
+
client.emit(
|
|
32
|
+
'reload',
|
|
33
|
+
changes.filter((change) =>
|
|
34
|
+
// Ensures only changes inside the templates directory are emitted
|
|
35
|
+
path
|
|
36
|
+
.resolve(absolutePathToTemplatesDirectory, change.filename)
|
|
37
|
+
.startsWith(absolutePathToTemplatesDirectory),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
changes = [];
|
|
43
|
+
}, 150);
|
|
44
|
+
|
|
45
|
+
const absolutePathToTemplatesDirectory = path.resolve(
|
|
46
|
+
process.cwd(),
|
|
47
|
+
templatesDirRelativePath,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const [dependencyGraph, updateDependencyGraph, { resolveDependentsOf }] =
|
|
51
|
+
await createDependencyGraph(absolutePathToTemplatesDirectory);
|
|
52
|
+
|
|
53
|
+
const watcher = watch('', {
|
|
54
|
+
ignoreInitial: true,
|
|
55
|
+
cwd: absolutePathToTemplatesDirectory,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const getFilesOutsideTemplatesDirectory = () =>
|
|
59
|
+
Object.keys(dependencyGraph).filter((p) =>
|
|
60
|
+
path.relative(absolutePathToTemplatesDirectory, p).startsWith('..'),
|
|
61
|
+
);
|
|
62
|
+
let filesOutsideTemplatesDirectory = getFilesOutsideTemplatesDirectory();
|
|
63
|
+
// adds in to be watched separately all of the files that are outside of
|
|
64
|
+
// the user's templates directory
|
|
65
|
+
for (const p of filesOutsideTemplatesDirectory) {
|
|
66
|
+
watcher.add(p);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const exit = async () => {
|
|
70
|
+
await watcher.close();
|
|
71
|
+
};
|
|
72
|
+
process.on('SIGINT', exit);
|
|
73
|
+
process.on('uncaughtException', exit);
|
|
74
|
+
|
|
75
|
+
watcher.on('all', async (event, relativePathToChangeTarget) => {
|
|
76
|
+
const file = relativePathToChangeTarget.split(path.sep);
|
|
77
|
+
if (file.length === 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const pathToChangeTarget = path.resolve(
|
|
81
|
+
absolutePathToTemplatesDirectory,
|
|
82
|
+
relativePathToChangeTarget,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await updateDependencyGraph(event, pathToChangeTarget);
|
|
86
|
+
|
|
87
|
+
const newFilesOutsideTemplatesDirectory =
|
|
88
|
+
getFilesOutsideTemplatesDirectory();
|
|
89
|
+
// updates the files outside of the user's templates directory by unwatching
|
|
90
|
+
// the inexistent ones and watching the new ones
|
|
91
|
+
//
|
|
92
|
+
// Update watched files outside templates directory to handle dependency changes
|
|
93
|
+
for (const p of filesOutsideTemplatesDirectory) {
|
|
94
|
+
if (!newFilesOutsideTemplatesDirectory.includes(p)) {
|
|
95
|
+
watcher.unwatch(p);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const p of newFilesOutsideTemplatesDirectory) {
|
|
99
|
+
if (!filesOutsideTemplatesDirectory.includes(p)) {
|
|
100
|
+
watcher.add(p);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
filesOutsideTemplatesDirectory = newFilesOutsideTemplatesDirectory;
|
|
104
|
+
|
|
105
|
+
changes.push({
|
|
106
|
+
event,
|
|
107
|
+
filename: relativePathToChangeTarget,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// These dependents are dependents resolved recursively, so even dependents of dependents
|
|
111
|
+
// will be notified of this change so that we ensure that things are updated in the preview.
|
|
112
|
+
for (const dependentPath of resolveDependentsOf(pathToChangeTarget)) {
|
|
113
|
+
changes.push({
|
|
114
|
+
event: 'change' as const,
|
|
115
|
+
filename: path.relative(
|
|
116
|
+
absolutePathToTemplatesDirectory,
|
|
117
|
+
dependentPath,
|
|
118
|
+
),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
reload();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return watcher;
|
|
125
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import type http from 'node:http';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type url from 'node:url';
|
|
5
|
+
import { lookup } from 'mime-types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the template directory path from the referer URL.
|
|
9
|
+
* @param referer - The referer header value
|
|
10
|
+
* @returns The template directory path, or null if not found
|
|
11
|
+
*/
|
|
12
|
+
const extractTemplatePathFromReferer = (referer: string): string | null => {
|
|
13
|
+
try {
|
|
14
|
+
const refererUrl = new URL(referer);
|
|
15
|
+
const previewMatch = refererUrl.pathname.match(/\/preview\/(.+)$/);
|
|
16
|
+
if (!previewMatch?.[1]) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const templateSlug = previewMatch[1];
|
|
21
|
+
return templateSlug.replace(/\.(tsx|jsx|ts|js)$/, '');
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively searches for a static file starting from the template directory
|
|
29
|
+
* and traversing up to the templates root directory.
|
|
30
|
+
* @param templateFullPath - Full path to the template directory
|
|
31
|
+
* @param templatesDirResolved - Resolved path to the templates root directory
|
|
32
|
+
* @param relativeFilePath - Relative path to the file within the static folder
|
|
33
|
+
* @returns Absolute path to the found file, or null if not found
|
|
34
|
+
*/
|
|
35
|
+
const findStaticFileRecursively = (
|
|
36
|
+
templateFullPath: string,
|
|
37
|
+
templatesDirResolved: string,
|
|
38
|
+
relativeFilePath: string,
|
|
39
|
+
): string | null => {
|
|
40
|
+
let currentDir = templateFullPath;
|
|
41
|
+
|
|
42
|
+
while (currentDir.startsWith(templatesDirResolved)) {
|
|
43
|
+
const staticPath = path.join(currentDir, 'static', relativeFilePath);
|
|
44
|
+
if (existsSync(staticPath)) {
|
|
45
|
+
return staticPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Move up one directory level
|
|
49
|
+
const parentDir = path.dirname(currentDir);
|
|
50
|
+
if (parentDir === currentDir) {
|
|
51
|
+
// Reached filesystem root
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
currentDir = parentDir;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const serveStaticFile = async (
|
|
61
|
+
res: http.ServerResponse,
|
|
62
|
+
parsedUrl: url.UrlWithParsedQuery,
|
|
63
|
+
staticDirRelativePath: string,
|
|
64
|
+
templatesDirRelativePath?: string,
|
|
65
|
+
req?: http.IncomingMessage,
|
|
66
|
+
) => {
|
|
67
|
+
const originalPath = parsedUrl.pathname!;
|
|
68
|
+
const pathname = originalPath.startsWith('/static')
|
|
69
|
+
? originalPath.replace('/static', './static')
|
|
70
|
+
: `./static${originalPath}`;
|
|
71
|
+
const ext = path.parse(pathname).ext;
|
|
72
|
+
|
|
73
|
+
const staticBaseDir = path.resolve(process.cwd(), staticDirRelativePath);
|
|
74
|
+
let fileAbsolutePath: string | null = null;
|
|
75
|
+
|
|
76
|
+
if (templatesDirRelativePath && req?.headers.referer) {
|
|
77
|
+
const templateDirPath = extractTemplatePathFromReferer(req.headers.referer);
|
|
78
|
+
|
|
79
|
+
if (templateDirPath) {
|
|
80
|
+
const templatesDir = path.resolve(
|
|
81
|
+
process.cwd(),
|
|
82
|
+
templatesDirRelativePath,
|
|
83
|
+
);
|
|
84
|
+
const templateFullPath = path.join(templatesDir, templateDirPath);
|
|
85
|
+
const relativeFilePath = pathname.replace('./static/', '');
|
|
86
|
+
const templatesDirResolved = path.resolve(templatesDir);
|
|
87
|
+
|
|
88
|
+
fileAbsolutePath = findStaticFileRecursively(
|
|
89
|
+
templateFullPath,
|
|
90
|
+
templatesDirResolved,
|
|
91
|
+
relativeFilePath,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback to root static directory
|
|
97
|
+
if (!fileAbsolutePath) {
|
|
98
|
+
fileAbsolutePath = path.resolve(staticBaseDir, pathname);
|
|
99
|
+
if (!fileAbsolutePath.startsWith(staticBaseDir)) {
|
|
100
|
+
res.statusCode = 403;
|
|
101
|
+
res.end();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const fileHandle = await fs.open(fileAbsolutePath, 'r');
|
|
108
|
+
|
|
109
|
+
const fileData = await fs.readFile(fileHandle);
|
|
110
|
+
|
|
111
|
+
// if the file is found, set Content-type and send data
|
|
112
|
+
res.setHeader('Content-type', lookup(ext) || 'text/plain');
|
|
113
|
+
res.end(fileData);
|
|
114
|
+
|
|
115
|
+
void fileHandle.close();
|
|
116
|
+
} catch (exception) {
|
|
117
|
+
if (!existsSync(fileAbsolutePath)) {
|
|
118
|
+
res.statusCode = 404;
|
|
119
|
+
res.end();
|
|
120
|
+
} else {
|
|
121
|
+
const sanitizedFilePath = fileAbsolutePath.replace(/\n|\r/g, '');
|
|
122
|
+
console.error(
|
|
123
|
+
`Could not read file at %s to be served, here's the exception:`,
|
|
124
|
+
sanitizedFilePath,
|
|
125
|
+
exception,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
res.statusCode = 500;
|
|
129
|
+
res.end(
|
|
130
|
+
'Could not read file to be served! Check your terminal for more information.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import url from 'node:url';
|
|
4
|
+
import { createJiti } from 'jiti';
|
|
5
|
+
import logSymbols from 'log-symbols';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import { getPreviewServerLocation } from '../get-preview-server-location.js';
|
|
8
|
+
import { packageJson } from '../packageJson.js';
|
|
9
|
+
import { registerSpinnerAutostopping } from '../register-spinner-autostopping.js';
|
|
10
|
+
import { styleText } from '../style-text.js';
|
|
11
|
+
import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app.js';
|
|
12
|
+
import { serveStaticFile } from './serve-static-file.js';
|
|
13
|
+
|
|
14
|
+
let devServer: http.Server | undefined;
|
|
15
|
+
|
|
16
|
+
const safeAsyncServerListen = (server: http.Server, port: number) => {
|
|
17
|
+
return new Promise<{ portAlreadyInUse: boolean }>((resolve) => {
|
|
18
|
+
server.listen(port, () => {
|
|
19
|
+
resolve({ portAlreadyInUse: false });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.on('error', (e: NodeJS.ErrnoException) => {
|
|
23
|
+
if (e.code === 'EADDRINUSE') {
|
|
24
|
+
resolve({ portAlreadyInUse: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const startDevServer = async (
|
|
31
|
+
templatesDirRelativePath: string,
|
|
32
|
+
staticBaseDirRelativePath: string,
|
|
33
|
+
port: number,
|
|
34
|
+
): Promise<http.Server> => {
|
|
35
|
+
const [majorNodeVersion] = process.versions.node.split('.');
|
|
36
|
+
if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) {
|
|
37
|
+
console.error(
|
|
38
|
+
` ${logSymbols.error} Node ${majorNodeVersion} is not supported. Please upgrade to Node 20 or higher.`,
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const previewServerLocation = await getPreviewServerLocation();
|
|
44
|
+
const previewServer = createJiti(previewServerLocation);
|
|
45
|
+
|
|
46
|
+
devServer = http.createServer((req, res) => {
|
|
47
|
+
if (!req.url) {
|
|
48
|
+
res.end(404);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parsedUrl = url.parse(req.url, true);
|
|
53
|
+
|
|
54
|
+
// Never cache anything to avoid
|
|
55
|
+
res.setHeader(
|
|
56
|
+
'Cache-Control',
|
|
57
|
+
'no-cache, max-age=0, must-revalidate, no-store',
|
|
58
|
+
);
|
|
59
|
+
res.setHeader('Pragma', 'no-cache');
|
|
60
|
+
res.setHeader('Expires', '-1');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (
|
|
64
|
+
parsedUrl.path &&
|
|
65
|
+
!parsedUrl.path.startsWith('/preview/') &&
|
|
66
|
+
!parsedUrl.path.startsWith('/api/') &&
|
|
67
|
+
!parsedUrl.path.includes('_next/')
|
|
68
|
+
) {
|
|
69
|
+
void serveStaticFile(
|
|
70
|
+
res,
|
|
71
|
+
parsedUrl,
|
|
72
|
+
staticBaseDirRelativePath,
|
|
73
|
+
templatesDirRelativePath,
|
|
74
|
+
req,
|
|
75
|
+
);
|
|
76
|
+
} else if (!isNextReady) {
|
|
77
|
+
void nextReadyPromise.then(() =>
|
|
78
|
+
nextHandleRequest?.(req, res, parsedUrl),
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
void nextHandleRequest?.(req, res, parsedUrl);
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error('caught error', e);
|
|
85
|
+
|
|
86
|
+
res.writeHead(500);
|
|
87
|
+
res.end();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { portAlreadyInUse } = await safeAsyncServerListen(devServer, port);
|
|
92
|
+
|
|
93
|
+
if (!portAlreadyInUse) {
|
|
94
|
+
console.log(
|
|
95
|
+
styleText('greenBright', ` React PDF ${packageJson.version}`),
|
|
96
|
+
);
|
|
97
|
+
console.log(` Running preview at: http://localhost:${port}\n`);
|
|
98
|
+
} else {
|
|
99
|
+
const nextPortToTry = port + 1;
|
|
100
|
+
console.warn(
|
|
101
|
+
` ${logSymbols.warning} Port ${port} is already in use, trying ${nextPortToTry}`,
|
|
102
|
+
);
|
|
103
|
+
return startDevServer(
|
|
104
|
+
templatesDirRelativePath,
|
|
105
|
+
staticBaseDirRelativePath,
|
|
106
|
+
nextPortToTry,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
devServer.on('close', () => {
|
|
111
|
+
void app.close();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
devServer.on('error', (e: NodeJS.ErrnoException) => {
|
|
115
|
+
spinner.stopAndPersist({
|
|
116
|
+
symbol: logSymbols.error,
|
|
117
|
+
text: `Preview Server had an error: ${e.message}`,
|
|
118
|
+
});
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const spinner = ora({
|
|
123
|
+
text: 'Getting react-pdf preview server ready...\n',
|
|
124
|
+
prefixText: ' ',
|
|
125
|
+
}).start();
|
|
126
|
+
|
|
127
|
+
registerSpinnerAutostopping(spinner);
|
|
128
|
+
const timeBeforeNextReady = performance.now();
|
|
129
|
+
|
|
130
|
+
// these environment variables are used on the next app
|
|
131
|
+
// this is the most reliable way of communicating these paths through
|
|
132
|
+
process.env = {
|
|
133
|
+
NODE_ENV: 'development',
|
|
134
|
+
...(process.env as Omit<NodeJS.ProcessEnv, 'NODE_ENV'> & {
|
|
135
|
+
NODE_ENV?: NodeJS.ProcessEnv['NODE_ENV'];
|
|
136
|
+
}),
|
|
137
|
+
...getEnvVariablesForPreviewApp(
|
|
138
|
+
// Normalize paths to avoid issues with path resolution
|
|
139
|
+
path.normalize(templatesDirRelativePath),
|
|
140
|
+
previewServerLocation,
|
|
141
|
+
process.cwd(),
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const next = await previewServer.import<typeof import('next')['default']>(
|
|
146
|
+
'next',
|
|
147
|
+
{
|
|
148
|
+
default: true,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const app = next({
|
|
153
|
+
// passing in env here does not get the environment variables there
|
|
154
|
+
dev: false,
|
|
155
|
+
conf: {
|
|
156
|
+
images: {
|
|
157
|
+
// This is to avoid the warning with sharp
|
|
158
|
+
unoptimized: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
hostname: 'localhost',
|
|
162
|
+
port,
|
|
163
|
+
dir: previewServerLocation,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let isNextReady = false;
|
|
167
|
+
const nextReadyPromise = app.prepare();
|
|
168
|
+
try {
|
|
169
|
+
await nextReadyPromise;
|
|
170
|
+
} catch (exception) {
|
|
171
|
+
spinner.stopAndPersist({
|
|
172
|
+
symbol: logSymbols.error,
|
|
173
|
+
text: ` Preview Server had an error: ${exception}`,
|
|
174
|
+
});
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
isNextReady = true;
|
|
178
|
+
|
|
179
|
+
const nextHandleRequest:
|
|
180
|
+
| ReturnType<typeof app.getRequestHandler>
|
|
181
|
+
| undefined = app.getRequestHandler();
|
|
182
|
+
|
|
183
|
+
const secondsToNextReady = (
|
|
184
|
+
(performance.now() - timeBeforeNextReady) /
|
|
185
|
+
1000
|
|
186
|
+
).toFixed(1);
|
|
187
|
+
|
|
188
|
+
spinner.stopAndPersist({
|
|
189
|
+
text: `Ready in ${secondsToNextReady}s\n`,
|
|
190
|
+
symbol: logSymbols.success,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return devServer;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// based on https://stackoverflow.com/a/14032965
|
|
197
|
+
const makeExitHandler =
|
|
198
|
+
(
|
|
199
|
+
options?:
|
|
200
|
+
| { shouldKillProcess: false }
|
|
201
|
+
| { shouldKillProcess: true; killWithErrorCode: boolean },
|
|
202
|
+
) =>
|
|
203
|
+
(codeSignalOrError: number | NodeJS.Signals | Error) => {
|
|
204
|
+
if (typeof devServer !== 'undefined') {
|
|
205
|
+
console.log('\nshutting down dev server');
|
|
206
|
+
devServer.close();
|
|
207
|
+
devServer = undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (codeSignalOrError instanceof Error) {
|
|
211
|
+
console.error(codeSignalOrError);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (options?.shouldKillProcess) {
|
|
215
|
+
process.exit(options.killWithErrorCode ? 1 : 0);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// do something when app is closing
|
|
220
|
+
process.on('exit', makeExitHandler());
|
|
221
|
+
|
|
222
|
+
// catches ctrl+c event
|
|
223
|
+
process.on(
|
|
224
|
+
'SIGINT',
|
|
225
|
+
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// catches "kill pid" (for example: nodemon restart)
|
|
229
|
+
process.on(
|
|
230
|
+
'SIGUSR1',
|
|
231
|
+
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
|
|
232
|
+
);
|
|
233
|
+
process.on(
|
|
234
|
+
'SIGUSR2',
|
|
235
|
+
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: false }),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// catches uncaught exceptions
|
|
239
|
+
process.on(
|
|
240
|
+
'uncaughtException',
|
|
241
|
+
makeExitHandler({ shouldKillProcess: true, killWithErrorCode: true }),
|
|
242
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logSymbols from 'log-symbols';
|
|
2
|
+
import type { Ora } from 'ora';
|
|
3
|
+
|
|
4
|
+
const spinners = new Set<Ora>();
|
|
5
|
+
|
|
6
|
+
process.on('SIGINT', () => {
|
|
7
|
+
spinners.forEach((spinner) => {
|
|
8
|
+
if (spinner.isSpinning) {
|
|
9
|
+
spinner.stop();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
process.on('exit', (code) => {
|
|
15
|
+
if (code !== 0) {
|
|
16
|
+
spinners.forEach((spinner) => {
|
|
17
|
+
if (spinner.isSpinning) {
|
|
18
|
+
spinner.stopAndPersist({
|
|
19
|
+
symbol: logSymbols.error,
|
|
20
|
+
text: 'Process exited with error',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const registerSpinnerAutostopping = (spinner: Ora) => {
|
|
28
|
+
spinners.add(spinner);
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized fallback for Node versions (<20.12.0) without util.styleText.
|
|
3
|
+
* Returns the original text when styleText is unavailable.
|
|
4
|
+
*/
|
|
5
|
+
import * as nodeUtil from 'node:util';
|
|
6
|
+
|
|
7
|
+
type StyleTextFunction = typeof nodeUtil.styleText;
|
|
8
|
+
|
|
9
|
+
export const styleText: StyleTextFunction = (nodeUtil as any).styleText
|
|
10
|
+
? (nodeUtil as any).styleText
|
|
11
|
+
: (_: string, text: string) => text;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SYMBOLS = {
|
|
6
|
+
BRANCH: '├── ',
|
|
7
|
+
EMPTY: '',
|
|
8
|
+
INDENT: ' ',
|
|
9
|
+
LAST_BRANCH: '└── ',
|
|
10
|
+
VERTICAL: '│ ',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getTreeLines = async (
|
|
14
|
+
dirPath: string,
|
|
15
|
+
depth: number,
|
|
16
|
+
currentDepth = 0,
|
|
17
|
+
) => {
|
|
18
|
+
const base = process.cwd();
|
|
19
|
+
const dirFullpath = path.resolve(base, dirPath);
|
|
20
|
+
const dirname = path.basename(dirFullpath);
|
|
21
|
+
let lines = [dirname];
|
|
22
|
+
|
|
23
|
+
const dirStat = await fs.stat(dirFullpath);
|
|
24
|
+
if (dirStat.isDirectory() && currentDepth < depth) {
|
|
25
|
+
const childDirents = await fs.readdir(dirFullpath, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
childDirents.sort((a, b) => {
|
|
28
|
+
// orders directories before files
|
|
29
|
+
if (a.isDirectory() && b.isFile()) {
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (a.isFile() && b.isDirectory()) {
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// orders by name because they are the same type
|
|
38
|
+
// either directory & directory
|
|
39
|
+
// or file & file
|
|
40
|
+
return b.name > a.name ? -1 : 1;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < childDirents.length; i++) {
|
|
44
|
+
const dirent = childDirents[i]!;
|
|
45
|
+
const isLast = i === childDirents.length - 1;
|
|
46
|
+
|
|
47
|
+
const branchingSymbol = isLast ? SYMBOLS.LAST_BRANCH : SYMBOLS.BRANCH;
|
|
48
|
+
const verticalSymbol = isLast ? SYMBOLS.INDENT : SYMBOLS.VERTICAL;
|
|
49
|
+
|
|
50
|
+
if (dirent.isFile()) {
|
|
51
|
+
lines.push(`${branchingSymbol}${dirent.name}`);
|
|
52
|
+
} else {
|
|
53
|
+
const pathToDirectory = path.join(dirFullpath, dirent.name);
|
|
54
|
+
const treeLinesForSubDirectory = await getTreeLines(
|
|
55
|
+
pathToDirectory,
|
|
56
|
+
depth,
|
|
57
|
+
currentDepth + 1,
|
|
58
|
+
);
|
|
59
|
+
lines = lines.concat(
|
|
60
|
+
treeLinesForSubDirectory.map((line, index) =>
|
|
61
|
+
index === 0
|
|
62
|
+
? `${branchingSymbol}${line}`
|
|
63
|
+
: `${verticalSymbol}${line}`,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const tree = async (dirPath: string, depth: number) => {
|
|
74
|
+
const lines = await getTreeLines(dirPath, depth);
|
|
75
|
+
return lines.join(os.EOL);
|
|
76
|
+
};
|