@kudzujs/core 0.5.4 → 0.5.5

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/README.md CHANGED
@@ -281,7 +281,7 @@ const rows = items.map(item =>
281
281
  return <ul>{rows}</ul>
282
282
  ```
283
283
 
284
- The root may also be a top-level same-file row component. Kudzu specializes each call at build time, so projected props, callback props, and simple local calculations compile to the same intrinsic list template:
284
+ The root may also be a top-level row component declared in the same file or imported from a relative TypeScript module. Default, named, aliased, and named re-export imports are resolved at build time. Kudzu specializes each call, so projected props, callback props, and simple local calculations compile to the same intrinsic list template:
285
285
 
286
286
  ```tsx
287
287
  function ItemRow({ name, done, onRemove }: {
@@ -306,7 +306,7 @@ const rows = items.map(item => <ItemRow
306
306
 
307
307
  The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
308
308
 
309
- Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level same-file row component, and `key={item.<field>}`. Row components accept destructured projected props and top-level single-`const` calculations before one intrinsic return. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Exported or imported row components, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
309
+ Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. Row components accept destructured projected props and top-level single-`const` calculations before one intrinsic return. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package or namespace row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
310
310
 
311
311
  ## Normal JavaScript
312
312
 
@@ -29,10 +29,11 @@ export async function build({ quiet = false, minify = true } = {}) {
29
29
  const cssFiles = projectFiles.filter(file => file.endsWith(".css")).sort()
30
30
  if (!sourceFiles.length) throw new Error("No TypeScript files found in src/")
31
31
  const sourceFileSet = new Set(sourceFiles)
32
+ const sourceIndex = new Map(await Promise.all(sourceFiles.map(async file => [file, await readFile(file, "utf8")])))
32
33
 
33
34
  const handlerModules = []
34
35
  for (const file of sourceFiles) {
35
- const handlerModule = await compile(file, sourceFileSet, base)
36
+ const handlerModule = await compile(file, sourceFileSet, sourceIndex, base)
36
37
  if (handlerModule) handlerModules.push(handlerModule)
37
38
  }
38
39
 
@@ -369,8 +370,8 @@ function escapeHtml(value) {
369
370
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
370
371
  }
371
372
 
372
- async function compile(file, sourceFiles, base) {
373
- const source = await readFile(file, "utf8")
373
+ async function compile(file, sourceFiles, sourceIndex, base) {
374
+ const source = sourceIndex.get(file)
374
375
  const nativeHandlers = []
375
376
  const reactiveBindings = []
376
377
  const listExpressions = []
@@ -384,7 +385,7 @@ async function compile(file, sourceFiles, base) {
384
385
  jsx: ts.JsxEmit.ReactJSX,
385
386
  jsxImportSource: "@kudzujs/core"
386
387
  },
387
- transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, clientImports)] },
388
+ transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, clientImports)] },
388
389
  reportDiagnostics: true
389
390
  })
390
391
 
@@ -413,12 +414,22 @@ async function compile(file, sourceFiles, base) {
413
414
  return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, clientImports: [...clientImports] }
414
415
  }
415
416
 
416
- function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, clientImports) {
417
+ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports) {
417
418
  return context => sourceFile => {
418
419
  const factory = context.factory
419
420
  sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
420
421
  ts.setParentRecursive(sourceFile, false)
421
422
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
423
+ const importedSources = new Map()
424
+ const importedSource = target => {
425
+ let imported = importedSources.get(target)
426
+ if (!imported) {
427
+ imported = normalizeRenderControlFlow(parseSourceFile(target, sourceIndex.get(target)), factory, context)
428
+ ts.setParentRecursive(imported, false)
429
+ importedSources.set(target, imported)
430
+ }
431
+ return imported
432
+ }
422
433
  const settersByFunction = new Map()
423
434
  const functions = new Map()
424
435
  const components = new Map()
@@ -520,8 +531,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
520
531
  }
521
532
  collectRenderedLists(sourceFile)
522
533
  const fail = (node, message) => {
523
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
524
- throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
534
+ throw sourceNodeError(node, sourceFile, message)
525
535
  }
526
536
  const rejectUnsupportedRenderControl = node => {
527
537
  if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
@@ -540,13 +550,19 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
540
550
  const componentSpecializations = new WeakMap()
541
551
  const specializedDeclarations = new WeakSet()
542
552
  for (const name of listComponentNames) {
543
- const component = components.get(name)
544
- if (!component) fail(sourceFile, `Keyed list component ${name} must be declared at the top level in the same file`)
545
- if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
553
+ let component = components.get(name)
554
+ const local = Boolean(component)
555
+ if (!component) {
556
+ const binding = importBindings.get(name)
557
+ if (!binding || binding.kind === "namespace") fail(sourceFile, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
558
+ const imported = binding.kind === "default" ? "default" : binding.imported
559
+ component = { function: resolveComponentExport(binding.target, imported, importedSource, sourceFiles), declaration: undefined }
560
+ }
561
+ if (local && isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
546
562
  const calls = jsxTagUses(sourceFile, name)
547
- if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
563
+ if (local && identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
548
564
  for (const call of calls) componentSpecializations.set(call, specializeComponentCall(call, component.function, sourceFile, factory, context, fail))
549
- specializedDeclarations.add(component.declaration)
565
+ if (local) specializedDeclarations.add(component.declaration)
550
566
  }
551
567
  const renderedLists = new WeakMap()
552
568
  for (const { node, parts: originalParts } of rawRenderedLists) {
@@ -603,11 +619,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
603
619
  if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
604
620
 
605
621
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
606
- return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(modulePath(node.moduleSpecifier.text)), node.attributes)
622
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
623
+ return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
607
624
  }
608
625
 
609
626
  if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
610
- return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(modulePath(node.moduleSpecifier.text)), node.attributes)
627
+ const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
628
+ return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
611
629
  }
612
630
 
613
631
  if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState" && node.initializer.arguments.length === 1) {
@@ -677,7 +695,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
677
695
  }
678
696
  }
679
697
 
680
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.getText()) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
698
+ if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
681
699
  const expression = node.initializer.expression
682
700
  const setters = settersForNode(node, settersByFunction)
683
701
  const usedStates = referencedStateNames(expression, setters)
@@ -690,15 +708,16 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
690
708
  }
691
709
  }
692
710
 
693
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.getText())) {
711
+ if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
694
712
  const setters = settersForNode(node, settersByFunction)
695
713
  const event = compileEvent(node.initializer.expression, setters, functions, factory, nativeHandlers, handlerUrl, listEventItems.get(node), importBindings, clientImports)
696
714
  if (event) {
697
715
  usesBehavior = true
698
716
  return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
699
717
  }
718
+ if (ts.isIdentifier(node.initializer.expression) && isDestructuredParameter(node.initializer.expression, nearestFunction(node))) return node
700
719
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
701
- throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.getText()} must reference a function`)
720
+ throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.text} must reference a function`)
702
721
  }
703
722
 
704
723
  return ts.visitEachChild(node, visitor, context)
@@ -854,8 +873,7 @@ function keyedListParts(expression, setters) {
854
873
 
855
874
  function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
856
875
  const fail = (node, message) => {
857
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
858
- throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
876
+ throw sourceNodeError(node, sourceFile, message)
859
877
  }
860
878
  const root = parts.root
861
879
  const item = parts.item
@@ -869,7 +887,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
869
887
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
870
888
  if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed lists are not supported")
871
889
  if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
872
- if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText())) {
890
+ if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
873
891
  listEventItems.set(node, item)
874
892
  return
875
893
  }
@@ -888,9 +906,9 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
888
906
  return
889
907
  }
890
908
  const field = directProperty(expression, item)
891
- const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.getText() === "key"
909
+ const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.text === "key"
892
910
  if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
893
- if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
911
+ if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
894
912
  if (isRootKey) return
895
913
  if (field) {
896
914
  listValues.set(node.expression, { field })
@@ -898,7 +916,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
898
916
  }
899
917
  if (referencesIdentifier(expression, item)) {
900
918
  validateListExpression(expression, item, node, fail)
901
- if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
919
+ if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
902
920
  listValues.set(node.expression, { item })
903
921
  return
904
922
  }
@@ -971,6 +989,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
971
989
 
972
990
  function substituteClone(root, substitutions, factory, context) {
973
991
  const visit = (node, shadowed = new Set()) => {
992
+ if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
974
993
  if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
975
994
  return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
976
995
  }
@@ -1026,6 +1045,10 @@ function isFunctionLike(node) {
1026
1045
  return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
1027
1046
  }
1028
1047
 
1048
+ function isDestructuredParameter(identifier, fn) {
1049
+ return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
1050
+ }
1051
+
1029
1052
  function isExportedDeclaration(node) {
1030
1053
  const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
1031
1054
  return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
@@ -1057,6 +1080,7 @@ const assignmentOperators = new Set([
1057
1080
 
1058
1081
  function validateListExpression(expression, item, source, fail) {
1059
1082
  const visit = node => {
1083
+ if (ts.isTypeNode(node)) return
1060
1084
  if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
1061
1085
  const key = node.argumentExpression
1062
1086
  if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
@@ -1399,6 +1423,60 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
1399
1423
  return bindings
1400
1424
  }
1401
1425
 
1426
+ function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
1427
+ const key = `${file}:${exportName}`
1428
+ if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
1429
+ const sourceFile = getSource(file)
1430
+ const nextTrail = [...trail, key]
1431
+
1432
+ for (const statement of sourceFile.statements) {
1433
+ if (ts.isFunctionDeclaration(statement)) {
1434
+ const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
1435
+ const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
1436
+ if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
1437
+ }
1438
+ if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
1439
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
1440
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
1441
+ }
1442
+ if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
1443
+ const component = localComponentDeclaration(sourceFile, statement.expression.text)
1444
+ if (component) return component
1445
+ }
1446
+ if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
1447
+ const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
1448
+ if (!entry) continue
1449
+ const imported = (entry.propertyName ?? entry.name).text
1450
+ if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
1451
+ if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
1452
+ const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
1453
+ return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
1454
+ }
1455
+ const component = localComponentDeclaration(sourceFile, imported)
1456
+ if (component) return component
1457
+ }
1458
+ }
1459
+ throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
1460
+ }
1461
+
1462
+ function localComponentDeclaration(sourceFile, name) {
1463
+ for (const statement of sourceFile.statements) {
1464
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement
1465
+ if (ts.isVariableStatement(statement)) {
1466
+ const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === name)
1467
+ if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
1468
+ }
1469
+ }
1470
+ return undefined
1471
+ }
1472
+
1473
+ function sourceNodeError(node, fallbackSource, message) {
1474
+ const original = ts.getOriginalNode(node)
1475
+ const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
1476
+ const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
1477
+ return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
1478
+ }
1479
+
1402
1480
  function printClientImports(entries, handlerPath) {
1403
1481
  const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
1404
1482
  const groups = Map.groupBy(unique.values(), entry => entry.target)
@@ -1474,7 +1552,7 @@ function resolveSourceImport(importer, specifier, sourceFiles) {
1474
1552
  const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
1475
1553
  const candidates = extension === ".ts" || extension === ".tsx"
1476
1554
  ? [base]
1477
- : [`${stem}.ts`, `${stem}.tsx`]
1555
+ : [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
1478
1556
  const matches = candidates.filter(candidate => sourceFiles.has(candidate))
1479
1557
  if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
1480
1558
  return matches[0]
@@ -1667,11 +1745,6 @@ function numericExpression(factory, value, negative) {
1667
1745
  return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
1668
1746
  }
1669
1747
 
1670
- function modulePath(value) {
1671
- if (/\.(?:ts|tsx|js|jsx)$/.test(value)) return value.replace(/\.(?:ts|tsx|js|jsx)$/, ".mjs")
1672
- return `${value}.mjs`
1673
- }
1674
-
1675
1748
  function compiledPath(file) {
1676
1749
  return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
1677
1750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",