|
1 |
| -import { imports, exports, common } from '@svelte-cli/core/js'; |
2 |
| -import type { ScriptFileEditor } from '@svelte-cli/core'; |
| 1 | +import { imports, exports, common, variables, functions } from '@svelte-cli/core/js'; |
| 2 | +import { Walker, type AstKinds, type AstTypes, type ScriptFileEditor } from '@svelte-cli/core'; |
3 | 3 | import type { Question } from '@svelte-cli/core/internal';
|
4 | 4 |
|
| 5 | +export function createPrinter(...conditions: boolean[]) { |
| 6 | + const printers = conditions.map((condition) => { |
| 7 | + return (content: string, alt = '') => (condition ? content : alt); |
| 8 | + }); |
| 9 | + return printers; |
| 10 | +} |
| 11 | + |
5 | 12 | export function addEslintConfigPrettier({ ast }: ScriptFileEditor<Record<string, Question>>) {
|
6 | 13 | // if a default import for `eslint-plugin-svelte` already exists, then we'll use their specifier's name instead
|
7 | 14 | const importNodes = ast.body.filter((n) => n.type === 'ImportDeclaration');
|
@@ -59,3 +66,294 @@ export function addEslintConfigPrettier({ ast }: ScriptFileEditor<Record<string,
|
59 | 66 | elements.push(...nodesToInsert);
|
60 | 67 | }
|
61 | 68 | }
|
| 69 | + |
| 70 | +export function addGlobalAppInterface( |
| 71 | + ast: AstTypes.Program, |
| 72 | + name: 'Error' | 'Locals' | 'PageData' | 'PageState' | 'Platform' |
| 73 | +) { |
| 74 | + let globalDecl = ast.body |
| 75 | + .filter((n) => n.type === 'TSModuleDeclaration') |
| 76 | + .find((m) => m.global && m.declare); |
| 77 | + |
| 78 | + if (!globalDecl) { |
| 79 | + globalDecl = common.statementFromString('declare global {}') as AstTypes.TSModuleDeclaration; |
| 80 | + ast.body.push(globalDecl); |
| 81 | + } |
| 82 | + |
| 83 | + if (globalDecl.body?.type !== 'TSModuleBlock') { |
| 84 | + throw new Error('Unexpected body type of `declare global` in `src/app.d.ts`'); |
| 85 | + } |
| 86 | + |
| 87 | + let app: AstTypes.TSModuleDeclaration | undefined; |
| 88 | + let interfaceNode: AstTypes.TSInterfaceDeclaration | undefined; |
| 89 | + |
| 90 | + // prettier-ignore |
| 91 | + Walker.walk(globalDecl as AstTypes.ASTNode, {}, { |
| 92 | + TSModuleDeclaration(node, { next }) { |
| 93 | + if (node.id.type === 'Identifier' && node.id.name === 'App') { |
| 94 | + app = node; |
| 95 | + } |
| 96 | + next(); |
| 97 | + }, |
| 98 | + TSInterfaceDeclaration(node) { |
| 99 | + if (node.id.type === 'Identifier' && node.id.name === name) { |
| 100 | + interfaceNode = node; |
| 101 | + } |
| 102 | + }, |
| 103 | + }); |
| 104 | + |
| 105 | + if (!app) { |
| 106 | + app = common.statementFromString('namespace App {}') as AstTypes.TSModuleDeclaration; |
| 107 | + globalDecl.body.body.push(app); |
| 108 | + } |
| 109 | + |
| 110 | + if (app.body?.type !== 'TSModuleBlock') { |
| 111 | + throw new Error('Unexpected body type of `namespace App` in `src/app.d.ts`'); |
| 112 | + } |
| 113 | + |
| 114 | + if (!interfaceNode) { |
| 115 | + // add the interface if it's missing |
| 116 | + interfaceNode = common.statementFromString( |
| 117 | + `interface ${name} {}` |
| 118 | + ) as AstTypes.TSInterfaceDeclaration; |
| 119 | + app.body.body.push(interfaceNode); |
| 120 | + } |
| 121 | + |
| 122 | + return interfaceNode; |
| 123 | +} |
| 124 | + |
| 125 | +export function hasTypeProp( |
| 126 | + name: string, |
| 127 | + node: AstTypes.TSInterfaceDeclaration['body']['body'][number] |
| 128 | +) { |
| 129 | + return ( |
| 130 | + node.type === 'TSPropertySignature' && node.key.type === 'Identifier' && node.key.name === name |
| 131 | + ); |
| 132 | +} |
| 133 | + |
| 134 | +export function addHooksHandle( |
| 135 | + ast: AstTypes.Program, |
| 136 | + typescript: boolean, |
| 137 | + newHandleName: string, |
| 138 | + handleContent: string, |
| 139 | + forceSeparateHandle: boolean = false |
| 140 | +) { |
| 141 | + if (typescript) { |
| 142 | + imports.addNamed(ast, '@sveltejs/kit', { Handle: 'Handle' }, true); |
| 143 | + } |
| 144 | + |
| 145 | + let isSpecifier: boolean = false; |
| 146 | + let handleName = 'handle'; |
| 147 | + let exportDecl: AstTypes.ExportNamedDeclaration | undefined; |
| 148 | + let originalHandleDecl: AstKinds.DeclarationKind | undefined; |
| 149 | + |
| 150 | + // We'll first visit all of the named exports and grab their references if they export `handle`. |
| 151 | + // This will grab export references for: |
| 152 | + // `export { handle }` & `export { foo as handle }` |
| 153 | + // `export const handle = ...`, & `export function handle() {...}` |
| 154 | + // prettier-ignore |
| 155 | + Walker.walk(ast as AstTypes.ASTNode, {}, { |
| 156 | + ExportNamedDeclaration(node) { |
| 157 | + let maybeHandleDecl: AstKinds.DeclarationKind | undefined; |
| 158 | + |
| 159 | + // `export { handle }` & `export { foo as handle }` |
| 160 | + const handleSpecifier = node.specifiers?.find((s) => s.exported.name === 'handle'); |
| 161 | + if (handleSpecifier) { |
| 162 | + isSpecifier = true; |
| 163 | + // we'll search for the local name in case it's aliased (e.g. `export { foo as handle }`) |
| 164 | + handleName = handleSpecifier.local?.name ?? handleSpecifier.exported.name; |
| 165 | + |
| 166 | + // find the definition |
| 167 | + const handleFunc = ast.body.find((n) => isFunctionDeclaration(n, handleName)); |
| 168 | + const handleVar = ast.body.find((n) => isVariableDeclaration(n, handleName)); |
| 169 | + |
| 170 | + maybeHandleDecl = handleFunc ?? handleVar; |
| 171 | + } |
| 172 | + |
| 173 | + maybeHandleDecl ??= node.declaration ?? undefined; |
| 174 | + |
| 175 | + // `export const handle` |
| 176 | + if (maybeHandleDecl && isVariableDeclaration(maybeHandleDecl, handleName)) { |
| 177 | + exportDecl = node; |
| 178 | + originalHandleDecl = maybeHandleDecl; |
| 179 | + } |
| 180 | + |
| 181 | + // `export function handle` |
| 182 | + if (maybeHandleDecl && isFunctionDeclaration(maybeHandleDecl, handleName)) { |
| 183 | + exportDecl = node; |
| 184 | + originalHandleDecl = maybeHandleDecl; |
| 185 | + } |
| 186 | + }, |
| 187 | + }); |
| 188 | + |
| 189 | + const newHandle = common.expressionFromString(handleContent); |
| 190 | + if (common.hasNode(ast, newHandle)) return; |
| 191 | + |
| 192 | + // This is the straightforward case. If there's no existing `handle`, we'll just add one |
| 193 | + // with the new handle's definition and exit |
| 194 | + if (!originalHandleDecl || !exportDecl) { |
| 195 | + // handle declaration doesn't exist, so we'll just create it with the hook |
| 196 | + const newDecl = variables.declaration(ast, 'const', handleName, newHandle); |
| 197 | + if (typescript) { |
| 198 | + const declarator = newDecl.declarations[0] as AstTypes.VariableDeclarator; |
| 199 | + variables.typeAnnotateDeclarator(declarator, 'Handle'); |
| 200 | + } |
| 201 | + |
| 202 | + if (!forceSeparateHandle) exports.namedExport(ast, handleName, newDecl); |
| 203 | + else { |
| 204 | + const newDecl = variables.declaration(ast, 'const', newHandleName, newHandle); |
| 205 | + if (typescript) { |
| 206 | + const declarator = newDecl.declarations[0] as AstTypes.VariableDeclarator; |
| 207 | + variables.typeAnnotateDeclarator(declarator, 'Handle'); |
| 208 | + } |
| 209 | + ast.body.push(newDecl); |
| 210 | + |
| 211 | + const handleDecl = variables.declaration( |
| 212 | + ast, |
| 213 | + 'const', |
| 214 | + handleName, |
| 215 | + common.expressionFromString(newHandleName) |
| 216 | + ); |
| 217 | + if (typescript) { |
| 218 | + const declarator = handleDecl.declarations[0] as AstTypes.VariableDeclarator; |
| 219 | + variables.typeAnnotateDeclarator(declarator, 'Handle'); |
| 220 | + } |
| 221 | + exports.namedExport(ast, handleName, handleDecl); |
| 222 | + } |
| 223 | + |
| 224 | + return; |
| 225 | + } |
| 226 | + |
| 227 | + // create the new handle |
| 228 | + const newDecl = variables.declaration(ast, 'const', newHandleName, newHandle); |
| 229 | + if (typescript) { |
| 230 | + const declarator = newDecl.declarations[0] as AstTypes.VariableDeclarator; |
| 231 | + variables.typeAnnotateDeclarator(declarator, 'Handle'); |
| 232 | + } |
| 233 | + |
| 234 | + // check if `handle` is using a sequence |
| 235 | + let sequence: AstTypes.CallExpression | undefined; |
| 236 | + if (originalHandleDecl.type === 'VariableDeclaration') { |
| 237 | + const handle = originalHandleDecl.declarations.find( |
| 238 | + (d) => d.type === 'VariableDeclarator' && usingSequence(d, handleName) |
| 239 | + ) as AstTypes.VariableDeclarator | undefined; |
| 240 | + |
| 241 | + sequence = handle?.init as AstTypes.CallExpression; |
| 242 | + } |
| 243 | + |
| 244 | + // If `handle` is already using a `sequence`, then we'll just create the new handle and |
| 245 | + // append the new handle name to the args of `sequence` |
| 246 | + // e.g. `export const handle = sequence(some, other, handles, newHandle);` |
| 247 | + if (sequence) { |
| 248 | + const hasNewArg = sequence.arguments.some( |
| 249 | + (arg) => arg.type === 'Identifier' && arg.name === newHandleName |
| 250 | + ); |
| 251 | + if (!hasNewArg) { |
| 252 | + sequence.arguments.push(variables.identifier(newHandleName)); |
| 253 | + } |
| 254 | + |
| 255 | + // removes the declarations so we can append them in the correct order |
| 256 | + ast.body = ast.body.filter( |
| 257 | + (n) => n !== originalHandleDecl && n !== exportDecl && n !== newDecl |
| 258 | + ); |
| 259 | + if (isSpecifier) { |
| 260 | + // if export specifiers are being used (e.g. `export { handle }`), then we'll want |
| 261 | + // need to also append original handle declaration as it's not part of the export declaration |
| 262 | + ast.body.push(newDecl, originalHandleDecl, exportDecl); |
| 263 | + } else { |
| 264 | + ast.body.push(newDecl, exportDecl); |
| 265 | + } |
| 266 | + |
| 267 | + return; |
| 268 | + } |
| 269 | + |
| 270 | + // At this point, the existing `handle` doesn't call `sequence`, so we'll need to rename the original |
| 271 | + // `handle` and create a new `handle` that uses `sequence` |
| 272 | + // e.g. `const handle = sequence(originalHandle, newHandle);` |
| 273 | + const NEW_HANDLE_NAME = 'originalHandle'; |
| 274 | + const sequenceCall = functions.callByIdentifier('sequence', [NEW_HANDLE_NAME, newHandleName]); |
| 275 | + const newHandleDecl = variables.declaration(ast, 'const', handleName, sequenceCall); |
| 276 | + |
| 277 | + imports.addNamed(ast, '@sveltejs/kit/hooks', { sequence: 'sequence' }); |
| 278 | + |
| 279 | + let renameRequired = false; |
| 280 | + // rename `export const handle` |
| 281 | + if (originalHandleDecl && isVariableDeclaration(originalHandleDecl, handleName)) { |
| 282 | + const handle = getVariableDeclarator(originalHandleDecl, handleName); |
| 283 | + if (handle && handle.id.type === 'Identifier' && handle.init?.type !== 'Identifier') { |
| 284 | + renameRequired = true; |
| 285 | + handle.id.name = NEW_HANDLE_NAME; |
| 286 | + } |
| 287 | + } |
| 288 | + // rename `export function handle` |
| 289 | + if (originalHandleDecl && isFunctionDeclaration(originalHandleDecl, handleName)) { |
| 290 | + renameRequired = true; |
| 291 | + originalHandleDecl.id!.name = NEW_HANDLE_NAME; |
| 292 | + } |
| 293 | + |
| 294 | + // removes all declarations so that we can re-append them in the correct order |
| 295 | + ast.body = ast.body.filter((n) => n !== originalHandleDecl && n !== exportDecl && n !== newDecl); |
| 296 | + |
| 297 | + if (isSpecifier) { |
| 298 | + ast.body.push(originalHandleDecl, newDecl, newHandleDecl, exportDecl); |
| 299 | + } |
| 300 | + |
| 301 | + if (exportDecl.declaration && renameRequired) { |
| 302 | + // when we re-append the declarations, we only want to add the declaration |
| 303 | + // of the (now renamed) original `handle` _without_ the `export` keyword: |
| 304 | + // e.g. `const originalHandle = ...;` |
| 305 | + ast.body.push(exportDecl.declaration, newDecl); |
| 306 | + // `export const handle = sequence(originalHandle, newHandle);` |
| 307 | + exports.namedExport(ast, handleName, newHandleDecl); |
| 308 | + } else if (exportDecl.declaration && isVariableDeclaration(originalHandleDecl, handleName)) { |
| 309 | + // if the previous value of `export const handle = ...` was an identifier |
| 310 | + // there is no need to rename the handle, we just need to add it to the sequence |
| 311 | + const variableDeclarator = getVariableDeclarator(originalHandleDecl, handleName); |
| 312 | + const sequenceCall = functions.callByIdentifier('sequence', [ |
| 313 | + (variableDeclarator?.init as AstTypes.Identifier).name, |
| 314 | + newHandleName |
| 315 | + ]); |
| 316 | + const newHandleDecl = variables.declaration(ast, 'const', handleName, sequenceCall); |
| 317 | + if (typescript) { |
| 318 | + const declarator = newHandleDecl.declarations[0] as AstTypes.VariableDeclarator; |
| 319 | + variables.typeAnnotateDeclarator(declarator, 'Handle'); |
| 320 | + } |
| 321 | + ast.body.push(newDecl); |
| 322 | + exports.namedExport(ast, handleName, newHandleDecl); |
| 323 | + } |
| 324 | +} |
| 325 | + |
| 326 | +function usingSequence(node: AstTypes.VariableDeclarator, handleName: string) { |
| 327 | + return ( |
| 328 | + node.id.type === 'Identifier' && |
| 329 | + node.id.name === handleName && |
| 330 | + node.init?.type === 'CallExpression' && |
| 331 | + node.init.callee.type === 'Identifier' && |
| 332 | + node.init.callee.name === 'sequence' |
| 333 | + ); |
| 334 | +} |
| 335 | + |
| 336 | +function isVariableDeclaration( |
| 337 | + node: AstTypes.ASTNode, |
| 338 | + variableName: string |
| 339 | +): node is AstTypes.VariableDeclaration { |
| 340 | + return ( |
| 341 | + node.type === 'VariableDeclaration' && getVariableDeclarator(node, variableName) !== undefined |
| 342 | + ); |
| 343 | +} |
| 344 | + |
| 345 | +function getVariableDeclarator( |
| 346 | + node: AstTypes.VariableDeclaration, |
| 347 | + handleName: string |
| 348 | +): AstTypes.VariableDeclarator | undefined { |
| 349 | + return node.declarations.find( |
| 350 | + (d) => d.type === 'VariableDeclarator' && d.id.type === 'Identifier' && d.id.name === handleName |
| 351 | + ) as AstTypes.VariableDeclarator | undefined; |
| 352 | +} |
| 353 | + |
| 354 | +function isFunctionDeclaration( |
| 355 | + node: AstTypes.ASTNode, |
| 356 | + funcName: string |
| 357 | +): node is AstTypes.FunctionDeclaration { |
| 358 | + return node.type === 'FunctionDeclaration' && node.id?.name === funcName; |
| 359 | +} |
0 commit comments