@kudzujs/core 0.8.15 → 0.8.17

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.
@@ -0,0 +1,118 @@
1
+ export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
2
+ const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
3
+ return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
4
+ }
5
+
6
+ export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
7
+ const commandEvents = new Set()
8
+ const nativeEvents = new Set()
9
+ const bindings = { count: 0, text: false, svgConditions: false }
10
+ const lists = {
11
+ count: 0,
12
+ styleCount: 0,
13
+ conditions: false,
14
+ svg: false,
15
+ deepConditions: false,
16
+ textRanges: false,
17
+ attributes: false,
18
+ events: false,
19
+ expressions: false,
20
+ expressionAttributes: false,
21
+ seeds: false,
22
+ effects: false,
23
+ rowHooks: false,
24
+ rowRefs: false,
25
+ complexRowState: false,
26
+ nested: false,
27
+ selectors: false,
28
+ calculated: false,
29
+ static: false,
30
+ indexes: false,
31
+ stableFastPaths: false,
32
+ generalRowHooks: false,
33
+ asyncParts: false,
34
+ mounts: false
35
+ }
36
+ const effects = { any: false, derivedDependencies: false, itemDependencies: false, captures: false, navigable: false, navigableOwners: false }
37
+
38
+ for (let index = 0; index < plans.length; index++) {
39
+ const plan = plans[index]
40
+ const route = routes.get(plan.route)
41
+ for (const event of plan.events) {
42
+ if (event.commands) commandEvents.add(event.event)
43
+ if (event.native) nativeEvents.add(event.event)
44
+ }
45
+ bindings.text ||= plan.bindings.some(binding => binding.target === "text")
46
+ bindings.svgConditions ||= plan.conditions.some(condition => condition.svg)
47
+ effects.any ||= plan.effects.length > 0
48
+ effects.derivedDependencies ||= plan.effects.some(effect => effect.dependencyExpressions?.length)
49
+ effects.itemDependencies ||= plan.effects.some(effect => effect.itemDependencies?.length)
50
+ effects.captures ||= plan.effects.some(effect => Object.keys(effect.scope).length)
51
+ effects.navigable ||= Boolean(route?.navigable && plan.effects.length)
52
+ effects.navigableOwners ||= Boolean(route?.navigable && plan.effects.some(effect => effect.owner))
53
+ for (const list of plan.lists) {
54
+ lists.conditions ||= Boolean(list.conditions)
55
+ lists.svg ||= Boolean(list.svg)
56
+ lists.deepConditions ||= Boolean(list.conditionHandlers)
57
+ lists.textRanges ||= Boolean(list.textRanges)
58
+ lists.attributes ||= Boolean(list.attributes)
59
+ lists.events ||= Boolean(list.events)
60
+ lists.expressions ||= Boolean(list.expressions)
61
+ lists.expressionAttributes ||= Boolean(list.expressionAttributes)
62
+ lists.seeds ||= Boolean(list.seed || list.valueSeed)
63
+ lists.effects ||= Boolean(list.effects)
64
+ lists.rowHooks ||= Boolean(list.rowStates?.length || list.rowRefs?.length)
65
+ lists.rowRefs ||= Boolean(list.rowRefs?.length)
66
+ lists.complexRowState ||= Boolean(list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object"))
67
+ lists.nested ||= Boolean(list.ownerField)
68
+ lists.selectors ||= Boolean(list.selector)
69
+ lists.calculated ||= Boolean(list.source)
70
+ lists.static ||= Boolean(list.static)
71
+ lists.indexes ||= Boolean(list.indexed)
72
+ lists.stableFastPaths ||= !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector
73
+ lists.generalRowHooks ||= Boolean(list.ownerField && (list.rowStates?.length || list.rowRefs?.length))
74
+ lists.mounts ||= Boolean(list.mount)
75
+ }
76
+ }
77
+
78
+ const routeEntries = [...routes.values()]
79
+ const routeCounts = {
80
+ behaviors: routeEntries.filter(route => route.hasBehaviors).length,
81
+ regularBehaviors: routeEntries.filter(route => route.hasBehaviors && !route.usesDependencyRuntime).length,
82
+ regularStateSeeds: routeEntries.filter(route => route.hasStateSeed && !route.usesDependencyRuntime).length,
83
+ dependencyStateSeeds: routeEntries.filter(route => route.hasStateSeed && route.usesDependencyRuntime).length
84
+ }
85
+ bindings.count = routeEntries.filter(route => route.hasBindings).length
86
+ lists.count = routeEntries.filter(route => route.hasLists).length
87
+ lists.styleCount = routeEntries.filter(route => route.hasListStyles).length
88
+ lists.generalRowHooks ||= lists.rowRefs || lists.complexRowState
89
+ lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
90
+ lists.mounts ||= lists.conditions || lists.nested
91
+
92
+ return {
93
+ routes: routeCounts,
94
+ events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
95
+ bindings,
96
+ lists,
97
+ effects,
98
+ captures: { nestedState: hasNestedCaptureState(plans), setter: hasCaptureType(plans, "setter") },
99
+ runtime: {
100
+ shared: Boolean(bindings.count || lists.count || nativeEvents.size || navigationRouteCount),
101
+ dependency: routeEntries.some(route => route.usesDependencyRuntime)
102
+ }
103
+ }
104
+ }
105
+
106
+ function hasCaptureType(value, type) {
107
+ if (!value || typeof value !== "object") return false
108
+ if (value.type === type) return true
109
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
110
+ }
111
+
112
+ function hasNestedCaptureState(value, insideCapture = false) {
113
+ if (!value || typeof value !== "object") return false
114
+ if (value.type === "state") return insideCapture
115
+ if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
116
+ if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
117
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
118
+ }
@@ -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.15",
3
+ "version": "0.8.17",
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"