@kudzujs/core 0.8.15 → 0.8.16
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/MIGRATION_ROADMAP.md +247 -0
- package/PERFORMANCE.md +212 -0
- package/README.md +1 -1
- package/RELEASES.md +32 -0
- package/docs/next-architecture/README.md +42 -0
- package/docs/next-architecture/compiler-current-architecture.md +71 -0
- package/docs/next-architecture/goal-a-compiler-foundation.md +152 -0
- package/docs/next-architecture/goal-b-optimization-benchmarks.md +59 -0
- package/docs/next-architecture/goal-c-state-resource-research.md +50 -0
- package/docs/next-architecture/goal-d-routing-compatibility-decisions.md +51 -0
- package/docs/next-architecture/performance-gates.md +50 -0
- package/docs/next-architecture/versioning.md +42 -0
- package/framework/README.md +6 -1
- package/framework/build.mjs +103 -617
- package/framework/compiler/collection-analysis.mjs +187 -0
- package/framework/compiler/descriptor-session.mjs +222 -0
- package/framework/compiler/event-command-pass.mjs +35 -0
- package/framework/compiler/react-migration-pass.mjs +3 -2
- package/framework/compiler/route-capability-planner.mjs +118 -0
- package/framework/compiler/zustand-pass.mjs +95 -0
- package/package.json +4 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { relative, sep } from "node:path"
|
|
2
|
+
import ts from "typescript"
|
|
3
|
+
import { isReferenceIdentifier, isShadowedIdentifier, nearestFunction, sourceNodeError, unwrapExpression } from "./ast-helpers.mjs"
|
|
4
|
+
|
|
5
|
+
export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory }) {
|
|
6
|
+
function analyzeZustandStores(sourceFile) {
|
|
7
|
+
const createNames = new Set()
|
|
8
|
+
for (const statement of sourceFile.statements) {
|
|
9
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "zustand") continue
|
|
10
|
+
const bindings = statement.importClause?.namedBindings
|
|
11
|
+
if (statement.importClause?.name || !bindings || !ts.isNamedImports(bindings)) throw sourceNodeError(statement, sourceFile, "Zustand migration input requires a named create import")
|
|
12
|
+
for (const entry of bindings.elements) {
|
|
13
|
+
if (entry.isTypeOnly) continue
|
|
14
|
+
if ((entry.propertyName ?? entry.name).text !== "create") throw sourceNodeError(entry, sourceFile, "Only Zustand create is supported")
|
|
15
|
+
createNames.add(entry.name.text)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const stores = new Map()
|
|
19
|
+
if (!createNames.size) return stores
|
|
20
|
+
for (const statement of sourceFile.statements) {
|
|
21
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
|
22
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
23
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !ts.isCallExpression(declaration.initializer) || !ts.isIdentifier(declaration.initializer.expression) || !createNames.has(declaration.initializer.expression.text)) continue
|
|
24
|
+
const callback = declaration.initializer.arguments[0]
|
|
25
|
+
if (declaration.initializer.arguments.length !== 1 || !callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name) || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(declaration.initializer, sourceFile, "Zustand create() requires one synchronous initializer with one set parameter")
|
|
26
|
+
const body = unwrapExpression(callback.body)
|
|
27
|
+
if (!ts.isObjectLiteralExpression(body)) throw sourceNodeError(callback.body, sourceFile, "Zustand create() initializer must return one object literal")
|
|
28
|
+
const data = []
|
|
29
|
+
const actions = new Map()
|
|
30
|
+
for (const property of body.properties) {
|
|
31
|
+
if (!ts.isPropertyAssignment(property) || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) throw sourceNodeError(property, sourceFile, "Zustand store entries must be ordinary properties")
|
|
32
|
+
const name = property.name.text
|
|
33
|
+
const value = unwrapExpression(property.initializer)
|
|
34
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) actions.set(name, value)
|
|
35
|
+
else data.push({ name, value })
|
|
36
|
+
}
|
|
37
|
+
if (data.length !== 1 || !isSerializableStateLiteral(data[0].value)) throw sourceNodeError(body, sourceFile, "Zustand migration stores require exactly one directly serializable data property")
|
|
38
|
+
if (!actions.size) throw sourceNodeError(body, sourceFile, "Zustand migration stores require at least one action")
|
|
39
|
+
for (const [name, action] of actions) {
|
|
40
|
+
if (action.asteriskToken || action.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
|
|
41
|
+
const capture = [...nativeCaptureNames(action, new Map())].find(entry => entry !== callback.parameters[0].name.text)
|
|
42
|
+
if (capture) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} cannot capture ${JSON.stringify(capture)}`)
|
|
43
|
+
const validateAction = node => {
|
|
44
|
+
if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
|
|
45
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["then", "catch", "finally"].includes(node.expression.name.text)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} cannot schedule asynchronous updates`)
|
|
46
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === callback.parameters[0].name.text && !isShadowedIdentifier(node.expression, action)) {
|
|
47
|
+
if (nearestFunction(node) !== action) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must call set directly`)
|
|
48
|
+
if (node.arguments.length !== 1) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} set() requires exactly one partial update`)
|
|
49
|
+
}
|
|
50
|
+
ts.forEachChild(node, validateAction)
|
|
51
|
+
}
|
|
52
|
+
validateAction(action.body)
|
|
53
|
+
}
|
|
54
|
+
stores.set(declaration.name.text, { name: declaration.name.text, setName: callback.parameters[0].name.text, field: data[0].name, initialValue: data[0].value, actions, declaration })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const visit = node => {
|
|
58
|
+
const recognized = ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node && [...stores.values()].some(store => store.declaration.initializer === node.parent)
|
|
59
|
+
if (ts.isIdentifier(node) && createNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !recognized) throw sourceNodeError(node, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
60
|
+
ts.forEachChild(node, visit)
|
|
61
|
+
}
|
|
62
|
+
visit(sourceFile)
|
|
63
|
+
return stores
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
|
|
67
|
+
const stores = analyzeZustandStores(sourceFile)
|
|
68
|
+
if (!stores.size) {
|
|
69
|
+
const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "zustand" && !statement.importClause?.isTypeOnly)
|
|
70
|
+
if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
71
|
+
return sourceFile
|
|
72
|
+
}
|
|
73
|
+
const identity = name => `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${name}`
|
|
74
|
+
const visitor = node => {
|
|
75
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && stores.has(node.name.text)) {
|
|
76
|
+
const store = stores.get(node.name.text)
|
|
77
|
+
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("__kCreateStore"), undefined, [
|
|
78
|
+
factory.createStringLiteral(identity(store.name)),
|
|
79
|
+
factory.createStringLiteral(store.field),
|
|
80
|
+
store.initialValue,
|
|
81
|
+
factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name)))
|
|
82
|
+
]))
|
|
83
|
+
}
|
|
84
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "zustand") return undefined
|
|
85
|
+
return ts.visitEachChild(node, visitor, context)
|
|
86
|
+
}
|
|
87
|
+
const normalized = ts.visitNode(sourceFile, visitor)
|
|
88
|
+
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("__kCreateStore"))])), factory.createStringLiteral("@kudzujs/core"))
|
|
89
|
+
const statements = [...normalized.statements]
|
|
90
|
+
statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
|
|
91
|
+
return factory.updateSourceFile(normalized, statements)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { analyzeZustandStores, normalizeZustandMigrationSyntax }
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudzujs/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.16",
|
|
4
4
|
"description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/",
|
|
26
|
+
"docs/next-architecture/",
|
|
26
27
|
"framework/",
|
|
27
28
|
"GOAL_A.md",
|
|
28
29
|
"GOAL_B.md",
|
|
30
|
+
"MIGRATION_ROADMAP.md",
|
|
31
|
+
"PERFORMANCE.md",
|
|
29
32
|
"RELEASES.md",
|
|
30
33
|
"README.md",
|
|
31
34
|
"LICENSE"
|