🌳
pt0/deployF/dockerF/clientServerSplitAI.mts
1import * as _ from 'lodash-es'
12import { tsAbsPath } from '../../ptDirF.mts'
13import path from 'path'
15const serverExportNames = ['getServerSideProps', 'getStaticProps', 'getStaticPaths']
17const resolveImportPath = async (importPath: string, fromPtPath: string) => {
18 if (!_.startsWith(importPath, '.')) return null
20 const parentDir = path.dirname(pathDownJoin(ptDir, fromPtPath))
21 const importAbsPath = path.resolve(parentDir, importPath)
23 if (await fileExists(tsAbsPath(importAbsPath))) {
24 return absPathToPtPath(tsAbsPath(importAbsPath))
25 }
27 for (const ext of ptFileExtA) {
28 const pathWithExt = importAbsPath + '.' + ext
29 if (await fileExists(tsAbsPath(pathWithExt))) {
30 return absPathToPtPath(tsAbsPath(pathWithExt))
31 }
32 }
33 return null
36export const getPageExportSources = async (pagePtPath: string) => {
37 const contents = await read1File(tsAbsPath(pathDownJoin(ptDir, pagePtPath)))
38 const ast = acornParse({ contents, ptPath: tsAbsPath(pagePtPath) })
40 const clientSources: string[] = []
41 const serverSources: string[] = []
43 // Collect all imports for lookup
44 const importMap: Record<string, string> = {}
45 for (const node of ast.body) {
46 if (node.type === 'ImportDeclaration') {
47 const resolvedPath = await resolveImportPath(node.source.value as string, pagePtPath)
48 if (!resolvedPath) continue
49 for (const spec of node.specifiers) {
50 importMap[spec.local.name] = resolvedPath
51 }
52 }
53 }
55 for (const node of ast.body) {
56 // Handle: export { X as default, getServerSideProps } from './path'
57 if (node.type === 'ExportNamedDeclaration' && node.source) {
58 const resolvedPath = await resolveImportPath(node.source.value as string, pagePtPath)
59 if (!resolvedPath) continue
61 for (const spec of node.specifiers) {
62 const exportedName = (spec.exported as {name: string}).name
63 if (exportedName === 'default') {
64 clientSources.push(resolvedPath)
65 } else if (serverExportNames.includes(exportedName)) {
66 serverSources.push(resolvedPath)
67 }
68 }
69 }
71 // Handle: export default SomeImportedComponent
72 if (node.type === 'ExportDefaultDeclaration') {
73 if (node.declaration.type === 'Identifier') {
74 const importedName = node.declaration.name
75 if (importMap[importedName]) {
76 clientSources.push(importMap[importedName])
77 } else {
78 clientSources.push(..._.values(importMap))
79 }
80 }
81 // Handle: export default function App() {} - the page itself is client code
82 if (node.declaration.type === 'FunctionDeclaration' ||
83 node.declaration.type === 'ArrowFunctionExpression' ||
84 node.declaration.type === 'ClassDeclaration') {
85 clientSources.push(..._.values(importMap))
86 }
87 }
88 }
90 return { clientSources: _.uniq(clientSources), serverSources: _.uniq(serverSources) }
93export const getClientServerPathSets = async (pagePathsA: string[]) => {
94 const allClientEntrypoints: string[] = []
95 const allServerEntrypoints: string[] = []
97 await allPromCalls(pagePathsA, async (pagePath) => {
98 if (!isPtCodeFileExt(pagePath)) return
99 if (_.includes(pagePath, '/api/')) return
100 if (_.endsWith(pagePath, '/route.js') || _.endsWith(pagePath, '/route.ts')) return
102 const { clientSources, serverSources } = await getPageExportSources(pagePath)
103 allClientEntrypoints.push(...clientSources)
104 allServerEntrypoints.push(...serverSources)
105 })
107 const clientImportGraph = await ptMadge(_.uniq(allClientEntrypoints))
108 const serverImportGraph = await ptMadge(_.uniq(allServerEntrypoints))
110 const clientPaths = new Set([
111 ...allClientEntrypoints,
112 ..._.chain(clientImportGraph).values().flatten().value()
113 ])
115 const serverOnlyPaths = new Set()
116 const serverPaths = [
117 ...allServerEntrypoints,
118 ..._.chain(serverImportGraph).values().flatten().value()
119 ]
121 for (const serverPath of serverPaths) {
122 if (!clientPaths.has(serverPath)) {
123 serverOnlyPaths.add(serverPath)
124 }
125 }
127 return { clientPaths, serverOnlyPaths }