@kudzujs/core 0.7.0 → 0.7.1

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
@@ -10,7 +10,7 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
10
10
 
11
11
  > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **0.7.0:** React-source migration preview. Supported conventional `react` imports now compile to static HTML and capability-specific ESM without React, a VDOM, or hydration. See [release notes](./RELEASES.md#070---react-source-migration-preview).
13
+ **0.7.1:** Vite-style landing assets. Common CSS, CSS Module, image, SVG, font, and `?url` imports now compile to static output without React, a VDOM, or hydration. See [release notes](./RELEASES.md#071---vite-style-landing-assets).
14
14
 
15
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
16
16
 
@@ -146,7 +146,7 @@ Static trusted HTML can be rendered without a transform layer:
146
146
 
147
147
  The HTML is intentionally not sanitized. Use only trusted or previously sanitized build-time content. Reactive raw HTML, children on the same element, void elements, and keyed-list raw HTML are rejected.
148
148
 
149
- Every CSS file under `src` is copied to the same relative path under `dist/assets` and linked in deterministic order. Configured root-relative URLs receive `base`; absolute HTTP URLs are preserved. A source style entry reads CSS, optionally transforms it, writes its declared output, and links it without an `afterBuild` file pipeline. `publicDir` defaults to `public` and may point elsewhere. Global or page `metadata` may be an object or a function of `{ route, params, props }`, so route props can set document language and head resources before rendering:
149
+ Every CSS file under `src` is emitted to the same relative path under `dist/assets` and linked in deterministic order. Relative side-effect CSS imports are erased after validation. Default `.module.css` imports become deterministic scoped class maps at build time, while default imports of `.avif`, `.gif`, `.ico`, `.jpeg`, `.jpg`, `.otf`, `.png`, `.svg`, `.ttf`, `.webp`, `.woff`, or `.woff2` become base-prefixed asset URL strings; the same files accept `?url`. Relative CSS `url(...)` references are rewritten to base-prefixed URLs, preserve query/hash suffixes, and copy the referenced bytes under their source-relative `dist/assets` path. Data, fragment, root-relative, protocol-relative, and absolute CSS URLs remain unchanged. Other import queries, import hashes, attributes, named/namespace asset imports, and CSS Module `composes` are rejected. Configured root-relative URLs receive `base`; absolute HTTP URLs are preserved. A source style entry reads CSS, optionally transforms it, writes its declared output, and links it without an `afterBuild` file pipeline. `publicDir` defaults to `public` and may point elsewhere. Global or page `metadata` may be an object or a function of `{ route, params, props }`, so route props can set document language and head resources before rendering:
150
150
 
151
151
  ```js
152
152
  export default {
package/RELEASES.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.1 - Vite-style landing assets
4
+
5
+ Kudzu 0.7.1 lets ordinary React/Vite landing-page source retain its common local stylesheet and static asset imports while preserving static HTML output and capability-only JavaScript.
6
+
7
+ ### New in 0.7.1
8
+
9
+ - Relative side-effect CSS imports are validated and erased before server module evaluation.
10
+ - Default CSS Module imports compile to deterministic scoped class maps with no client runtime.
11
+ - Relative image, SVG, and font imports compile to base-aware URL strings; supported assets also accept `?url`.
12
+ - Relative CSS `url(...)` references are rewritten to base-aware emitted URLs, preserving query and hash suffixes.
13
+ - Referenced asset bytes are copied under deterministic source-relative `dist/assets` paths.
14
+ - Static assets and CSS Modules work inside specialized relative keyed-row components.
15
+ - Declaration files under `src` remain available to TypeScript but are excluded from executable module compilation.
16
+ - The migration fixture verifies a non-root base, byte-identical assets, scoped classes, browser interaction, and zero JavaScript on its static route.
17
+
18
+ ### Boundary
19
+
20
+ CSS Module `composes`, arbitrary import queries, import hashes/attributes, and named or namespace asset bindings fail at build time with source locations. React, a VDOM, hydration, and a retained browser component tree remain absent.
21
+
22
+ ### Upgrade
23
+
24
+ ```bash
25
+ npm install @kudzujs/core@^0.7.1
26
+ ```
27
+
3
28
  ## 0.7.0 - React-source migration preview
4
29
 
5
30
  Kudzu 0.7.0 begins the migration track for ordinary React-shaped landing pages. Existing source may retain conventional supported imports from `react`; Kudzu rewrites those imports to compile-time APIs, pre-renders complete HTML, and emits only the route capabilities that are actually used. React, a virtual DOM, hydration, and a browser component tree are never emitted or executed.
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http"
2
2
  import { createHash, randomUUID } from "node:crypto"
3
- import { cp, mkdir, readFile, readdir, rm, stat, watch, writeFile } from "node:fs/promises"
4
- import { dirname, extname, join, relative, resolve, sep } from "node:path"
3
+ import { cp, mkdir, readFile, readdir, realpath, rm, stat, watch, writeFile } from "node:fs/promises"
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"
5
5
  import { pathToFileURL } from "node:url"
6
6
  import { build as bundle, transform } from "esbuild"
7
7
  import ts from "typescript"
@@ -13,6 +13,7 @@ const sourceDirectory = join(root, "src")
13
13
  const pagesDirectory = join(sourceDirectory, "pages")
14
14
  const workDirectory = join(root, ".kudzu")
15
15
  const outputDirectory = join(root, "dist")
16
+ const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
16
17
 
17
18
  const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\\/assets\\/kudzu(?:-(?:binding|list|native))?\\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
18
19
 
@@ -39,18 +40,22 @@ export async function build({ quiet = false, minify = true } = {}) {
39
40
  await mkdir(outputDirectory, { recursive: true })
40
41
 
41
42
  const projectFiles = await walk(sourceDirectory)
42
- const sourceFiles = projectFiles.filter(file => /\.(?:ts|tsx)$/.test(file)).sort()
43
+ const sourceFiles = projectFiles.filter(file => /\.(?:ts|tsx)$/.test(file) && !file.endsWith(".d.ts")).sort()
43
44
  const configuredStyleSources = new Set(configuredStyles.sources.map(style => style.source))
44
- const cssFiles = projectFiles.filter(file => file.endsWith(".css") && !configuredStyleSources.has(file)).sort()
45
+ const discoveredCssFiles = projectFiles.filter(file => file.toLowerCase().endsWith(".css") && !configuredStyleSources.has(file)).sort()
45
46
  if (!sourceFiles.length) throw new Error("No TypeScript files found in src/")
46
47
  const sourceFileSet = new Set(sourceFiles)
47
48
  const sourceIndex = new Map(await Promise.all(sourceFiles.map(async file => [file, await readFile(file, "utf8")])))
49
+ const staticFiles = await safeStaticFiles(projectFiles)
50
+ const cssFiles = orderSourceStyles(discoveredCssFiles, sourceFiles, sourceIndex, staticFiles)
51
+ const importedAssets = new Set()
52
+ const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
48
53
 
49
54
  const handlerModules = []
50
55
  const workerReferences = []
51
56
  for (const file of sourceFiles) {
52
57
  if (file.endsWith(".worker.ts")) continue
53
- const handlerModule = await compile(file, sourceFileSet, sourceIndex, base, workerReferences)
58
+ const handlerModule = await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences)
54
59
  if (handlerModule) handlerModules.push(handlerModule)
55
60
  }
56
61
 
@@ -387,7 +392,7 @@ export async function build({ quiet = false, minify = true } = {}) {
387
392
  for (const file of clientModules) {
388
393
  const output = join(assetsDirectory, clientModulePath(file))
389
394
  await mkdir(resolve(output, ".."), { recursive: true })
390
- await writeJavaScript(output, await compileClientModule(file, sourceFileSet), minify)
395
+ await writeJavaScript(output, await compileClientModule(file, sourceFileSet, staticFiles, importedAssets, cssModules, base), minify)
391
396
  }
392
397
  if (clientModules.length) {
393
398
  await bundle({
@@ -409,10 +414,20 @@ export async function build({ quiet = false, minify = true } = {}) {
409
414
  }
410
415
  const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
411
416
  await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
417
+ for (const file of new Set([...cssFiles, ...importedAssets])) {
418
+ const collision = join(publicDirectory, "assets", relative(sourceDirectory, file))
419
+ if (await exists(collision)) throw new Error(`${relative(root, collision)} collides with emitted source asset ${relative(root, file)}`)
420
+ }
412
421
  for (const file of cssFiles) {
413
422
  const output = join(assetsDirectory, relative(sourceDirectory, file))
414
423
  await mkdir(dirname(output), { recursive: true })
415
- await cp(file, output)
424
+ await writeFile(output, cssOutputs.get(file))
425
+ }
426
+ for (const file of [...importedAssets].sort()) {
427
+ if (cssOutputs.has(file)) continue
428
+ const output = join(assetsDirectory, relative(sourceDirectory, file))
429
+ await mkdir(dirname(output), { recursive: true })
430
+ await writeFile(output, await readFile(file))
416
431
  }
417
432
  for (const style of configuredStyles.sources) {
418
433
  let css = await readFile(style.source, "utf8")
@@ -1670,7 +1685,7 @@ function escapeAttribute(value) {
1670
1685
  return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
1671
1686
  }
1672
1687
 
1673
- async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1688
+ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
1674
1689
  const source = sourceIndex.get(file)
1675
1690
  const nativeHandlers = []
1676
1691
  const effectHandlers = []
@@ -1686,7 +1701,7 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
1686
1701
  jsx: ts.JsxEmit.ReactJSX,
1687
1702
  jsxImportSource: "@kudzujs/core"
1688
1703
  },
1689
- transformers: { before: [createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, clientImports, workerReferences)] },
1704
+ transformers: { before: [createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences)] },
1690
1705
  reportDiagnostics: true
1691
1706
  })
1692
1707
 
@@ -1729,7 +1744,7 @@ function hasReactModuleReference(source, file) {
1729
1744
  return found
1730
1745
  }
1731
1746
 
1732
- function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports, workerReferences) {
1747
+ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
1733
1748
  return context => sourceFile => {
1734
1749
  const factory = context.factory
1735
1750
  const hasLinkElements = /<link/i.test(sourceFile.text)
@@ -1901,10 +1916,23 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1901
1916
  usesRowState ||= specialization.rowStates.length > 0
1902
1917
  usesRowRef ||= specialization.rowRefs.length > 0
1903
1918
  }
1904
- const mergeSpecializedImports = (root, componentSource, call) => {
1919
+ const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
1905
1920
  const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
1906
1921
  for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
1907
1922
  const substitutions = new Map()
1923
+ for (const statement of componentSource.statements) {
1924
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !isStaticImport(statement.moduleSpecifier.text)) continue
1925
+ const entry = staticImportEntry(statement, componentSource, componentSource.fileName, staticFiles, importedAssets, cssModules, base, factory)
1926
+ if (!entry?.name) continue
1927
+ if (referenceIdentifiers(root, entry.name).length) substitutions.set(entry.name, entry.value)
1928
+ for (const effect of effects) {
1929
+ if (effect.source.getSourceFile() !== componentSource) continue
1930
+ if (!referenceIdentifiers(effect.call, entry.name).length) continue
1931
+ ts.setParentRecursive(effect.call, false)
1932
+ effect.call = substituteClone(effect.call, new Map([[entry.name, entry.value]]), factory, context)
1933
+ synthesizeTree(effect.call)
1934
+ }
1935
+ }
1908
1936
  for (const [name, entry] of componentImports) {
1909
1937
  const references = referenceIdentifiers(root, name)
1910
1938
  if (!references.length) continue
@@ -1939,7 +1967,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1939
1967
  for (const nestedCall of nestedCalls) {
1940
1968
  const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
1941
1969
  if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
1942
- nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall)
1970
+ nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
1943
1971
  synthesizeTree(nested.root)
1944
1972
  replacements.set(nestedCall, nested.root)
1945
1973
  count++
@@ -2022,7 +2050,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2022
2050
  const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
2023
2051
  registerRowHooks(call, specialization)
2024
2052
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
2025
- specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
2053
+ specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
2026
2054
  synthesizeTree(specialization.root)
2027
2055
  componentSpecializations.set(call, specialization)
2028
2056
  reducerComponentCalls.add(call)
@@ -2119,7 +2147,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2119
2147
  const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
2120
2148
  registerRowHooks(node, specialization)
2121
2149
  specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
2122
- if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node))
2150
+ if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
2123
2151
  expandedRowSpecializations.set(specialization.root, specialization)
2124
2152
  if (currentAggregate) {
2125
2153
  currentAggregate.effects.push(...specialization.effects)
@@ -2164,7 +2192,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2164
2192
  const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
2165
2193
  const componentSource = specialization.componentSource ?? sourceFile
2166
2194
  specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
2167
- if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root))
2195
+ if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
2168
2196
  if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
2169
2197
  const root = specialization.root
2170
2198
  let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
@@ -2228,6 +2256,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2228
2256
  }
2229
2257
 
2230
2258
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
2259
+ if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
2231
2260
  const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
2232
2261
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2233
2262
  }
@@ -2757,7 +2786,7 @@ function jsxCallHasReducerCallbackProp(call, reducers) {
2757
2786
  function runtimeImportNames(sourceFile, relative) {
2758
2787
  const names = new Set()
2759
2788
  for (const statement of sourceFile.statements) {
2760
- if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative) continue
2789
+ if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative || isStaticImport(statement.moduleSpecifier.text)) continue
2761
2790
  const clause = statement.importClause
2762
2791
  if (clause.name) names.add(clause.name.text)
2763
2792
  if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
@@ -3662,7 +3691,7 @@ function reducersForNode(node, reducersByFunction) {
3662
3691
  function clientImportBindings(sourceFile, file, sourceFiles) {
3663
3692
  const bindings = new Map()
3664
3693
  for (const node of sourceFile.statements) {
3665
- if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".")) continue
3694
+ if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".") || isStaticImport(node.moduleSpecifier.text)) continue
3666
3695
  let target
3667
3696
  try {
3668
3697
  target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
@@ -3857,6 +3886,7 @@ async function collectClientModules(entries, sourceFiles) {
3857
3886
  for (const node of sourceFile.statements) {
3858
3887
  if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
3859
3888
  if (!node.moduleSpecifier.text.startsWith(".")) throw new Error(`${relative(root, file)} Imported client helpers may only use relative runtime imports`)
3889
+ if (isStaticImport(node.moduleSpecifier.text)) continue
3860
3890
  queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
3861
3891
  }
3862
3892
  }
@@ -3869,12 +3899,13 @@ async function collectClientModules(entries, sourceFiles) {
3869
3899
  return [...modules].sort()
3870
3900
  }
3871
3901
 
3872
- async function compileClientModule(file, sourceFiles) {
3902
+ async function compileClientModule(file, sourceFiles, staticFiles, importedAssets, cssModules, base) {
3873
3903
  const source = await readFile(file, "utf8")
3874
3904
  const transformer = context => sourceFile => {
3875
3905
  const factory = context.factory
3876
3906
  const visitor = node => {
3877
3907
  if (ts.isImportDeclaration(node) && runtimeModuleReference(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
3908
+ if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
3878
3909
  const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
3879
3910
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
3880
3911
  }
@@ -3909,6 +3940,225 @@ function resolveSourceImport(importer, specifier, sourceFiles) {
3909
3940
  return matches[0]
3910
3941
  }
3911
3942
 
3943
+ function staticImportExtension(specifier) {
3944
+ return extname(specifier.split(/[?#]/, 1)[0]).toLowerCase()
3945
+ }
3946
+
3947
+ function isStaticImport(specifier) {
3948
+ const extension = staticImportExtension(specifier)
3949
+ return extension === ".css" || staticAssetExtensions.has(extension)
3950
+ }
3951
+
3952
+ function resolveStaticImport(importer, specifier, staticFiles) {
3953
+ const target = resolve(dirname(importer), specifier.split(/[?#]/, 1)[0])
3954
+ if (!staticFiles.has(target)) throw new Error(`${relative(root, importer)} Relative asset import ${JSON.stringify(specifier)} must resolve to an existing regular file under src/`)
3955
+ return target
3956
+ }
3957
+
3958
+ async function safeStaticFiles(files) {
3959
+ const sourceRoot = await realpath(sourceDirectory)
3960
+ const entries = await Promise.all(files.map(async file => {
3961
+ try {
3962
+ const target = await realpath(file)
3963
+ const path = relative(sourceRoot, target)
3964
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path) || !(await stat(target)).isFile()) return undefined
3965
+ return file
3966
+ } catch {
3967
+ return undefined
3968
+ }
3969
+ }))
3970
+ return new Set(entries.filter(Boolean))
3971
+ }
3972
+
3973
+ function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
3974
+ const ordered = []
3975
+ const seenStyles = new Set()
3976
+ const seenSources = new Set()
3977
+ const sourceSet = new Set(sourceFiles)
3978
+ const visit = file => {
3979
+ if (seenSources.has(file)) return
3980
+ seenSources.add(file)
3981
+ const sourceFile = parseSourceFile(file, sourceIndex.get(file))
3982
+ for (const statement of sourceFile.statements) {
3983
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
3984
+ const specifier = statement.moduleSpecifier.text
3985
+ if (staticImportExtension(specifier) === ".css") {
3986
+ let target
3987
+ try { target = resolveStaticImport(file, specifier, staticFiles) } catch { continue }
3988
+ if (!seenStyles.has(target)) {
3989
+ seenStyles.add(target)
3990
+ ordered.push(target)
3991
+ }
3992
+ continue
3993
+ }
3994
+ if (isStaticImport(specifier)) continue
3995
+ try { visit(resolveSourceImport(file, specifier, sourceSet)) } catch {}
3996
+ }
3997
+ }
3998
+ for (const file of sourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))) visit(file)
3999
+ for (const file of sourceFiles) visit(file)
4000
+ return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
4001
+ }
4002
+
4003
+ async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base) {
4004
+ const cssModules = new Map()
4005
+ const cssOutputs = new Map()
4006
+ for (const file of cssFiles) {
4007
+ let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base)
4008
+ if (file.toLowerCase().endsWith(".module.css")) {
4009
+ if (/\bcomposes\s*:/i.test(maskCssCommentsAndStrings(css))) throw new Error(`${relative(root, file)} CSS Modules composes is not supported`)
4010
+ const prefix = `k${createHash("sha256").update(relative(sourceDirectory, file).replaceAll(sep, "/")).digest("hex").slice(0, 8)}`
4011
+ css = (await transform(css, { loader: "local-css", sourcefile: `${prefix}.css`, target: "es2022" })).code
4012
+ const classes = {}
4013
+ for (const match of css.matchAll(new RegExp(`\\.${prefix}_([_a-zA-Z][_a-zA-Z0-9-]*)`, "g"))) classes[match[1]] = match[0].slice(1)
4014
+ cssModules.set(file, classes)
4015
+ }
4016
+ cssOutputs.set(file, css)
4017
+ }
4018
+ return { cssModules, cssOutputs }
4019
+ }
4020
+
4021
+ function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
4022
+ let output = ""
4023
+ let cursor = 0
4024
+ let index = 0
4025
+ while (index < css.length) {
4026
+ if (css.startsWith("/*", index)) {
4027
+ index = css.indexOf("*/", index + 2)
4028
+ index = index === -1 ? css.length : index + 2
4029
+ continue
4030
+ }
4031
+ if (css[index] === '"' || css[index] === "'") {
4032
+ index = cssStringEnd(css, index)
4033
+ continue
4034
+ }
4035
+ if (css.slice(index, index + 3).toLowerCase() !== "url" || /[-_a-z\d]/i.test(css[index - 1] ?? "")) {
4036
+ index++
4037
+ continue
4038
+ }
4039
+ let open = index + 3
4040
+ while (/\s/.test(css[open] ?? "")) open++
4041
+ if (css[open] !== "(") {
4042
+ index++
4043
+ continue
4044
+ }
4045
+ let start = open + 1
4046
+ while (/\s/.test(css[start] ?? "")) start++
4047
+ const quote = css[start] === '"' || css[start] === "'" ? css[start] : ""
4048
+ const valueStart = quote ? start + 1 : start
4049
+ let end = valueStart
4050
+ if (quote) {
4051
+ end = cssStringEnd(css, start) - 1
4052
+ if (css[end] !== quote) {
4053
+ index = open + 1
4054
+ continue
4055
+ }
4056
+ } else {
4057
+ while (end < css.length && css[end] !== ")") end += css[end] === "\\" ? 2 : 1
4058
+ }
4059
+ let close = quote ? end + 1 : end
4060
+ while (/\s/.test(css[close] ?? "")) close++
4061
+ if (css[close] !== ")") {
4062
+ index = open + 1
4063
+ continue
4064
+ }
4065
+ const value = css.slice(valueStart, end).trim()
4066
+ const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base)
4067
+ output += css.slice(cursor, index) + (replacement ?? css.slice(index, close + 1))
4068
+ cursor = close + 1
4069
+ index = close + 1
4070
+ }
4071
+ return output + css.slice(cursor)
4072
+ }
4073
+
4074
+ function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base) {
4075
+ if (!value || value.startsWith("/") || value.startsWith("#") || value.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(value)) return undefined
4076
+ const split = value.search(/[?#]/)
4077
+ const pathname = split === -1 ? value : value.slice(0, split)
4078
+ const suffix = split === -1 ? "" : value.slice(split)
4079
+ const target = resolve(dirname(file), pathname)
4080
+ if (!staticFiles.has(target)) throw new Error(`${relative(root, file)} CSS URL ${JSON.stringify(value)} must resolve to an existing regular file under src/`)
4081
+ importedAssets.add(target)
4082
+ const url = assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`)
4083
+ return `url(${quote || '"'}${url}${suffix}${quote || '"'})`
4084
+ }
4085
+
4086
+ function cssStringEnd(css, start) {
4087
+ const quote = css[start]
4088
+ let index = start + 1
4089
+ while (index < css.length) {
4090
+ if (css[index] === "\\") index += 2
4091
+ else if (css[index++] === quote) break
4092
+ else if (css[index - 1] === "\n") break
4093
+ }
4094
+ return index
4095
+ }
4096
+
4097
+ function maskCssCommentsAndStrings(css) {
4098
+ const masked = [...css]
4099
+ let index = 0
4100
+ while (index < css.length) {
4101
+ let end
4102
+ if (css.startsWith("/*", index)) {
4103
+ const close = css.indexOf("*/", index + 2)
4104
+ end = close === -1 ? css.length : close + 2
4105
+ } else if (css[index] === '"' || css[index] === "'") {
4106
+ end = cssStringEnd(css, index)
4107
+ } else {
4108
+ index++
4109
+ continue
4110
+ }
4111
+ for (; index < end; index++) if (masked[index] !== "\n") masked[index] = " "
4112
+ }
4113
+ return masked.join("")
4114
+ }
4115
+
4116
+ function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
4117
+ const specifier = node.moduleSpecifier.text
4118
+ if (specifier.includes("\\") || specifier.includes("#")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports require forward-slash paths without hash suffixes")
4119
+ const queryIndex = specifier.indexOf("?")
4120
+ const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
4121
+ if (query && query !== "url") throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports support only the ?url query")
4122
+ let target
4123
+ try {
4124
+ target = resolveStaticImport(file, specifier, staticFiles)
4125
+ } catch (error) {
4126
+ throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
4127
+ }
4128
+ if (node.attributes) throw sourceNodeError(node.attributes, sourceFile, "Static asset import attributes are not supported")
4129
+ const extension = staticImportExtension(specifier)
4130
+ if (query === "url") {
4131
+ if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
4132
+ if (extension !== ".css") importedAssets.add(target)
4133
+ const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
4134
+ return staticImportReplacement(node.importClause.name.text, value, factory)
4135
+ }
4136
+ if (extension === ".css") {
4137
+ const classes = cssModules.get(target)
4138
+ if (!node.importClause) return undefined
4139
+ if (!classes || !node.importClause.name || node.importClause.isTypeOnly || node.importClause.namedBindings) {
4140
+ const message = classes ? "CSS Modules require one default import" : "CSS imports must be side-effect imports"
4141
+ throw sourceNodeError(node.importClause, sourceFile, message)
4142
+ }
4143
+ const value = factory.createObjectLiteralExpression(Object.entries(classes).sort(([left], [right]) => left.localeCompare(right)).map(([name, scoped]) => factory.createPropertyAssignment(factory.createStringLiteral(name), factory.createStringLiteral(scoped))))
4144
+ return staticImportReplacement(node.importClause.name.text, value, factory)
4145
+ }
4146
+ if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
4147
+ importedAssets.add(target)
4148
+ const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
4149
+ return staticImportReplacement(node.importClause.name.text, value, factory)
4150
+ }
4151
+
4152
+ function staticImportReplacement(name, value, factory) {
4153
+ return {
4154
+ name,
4155
+ value,
4156
+ replacement: factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
4157
+ factory.createVariableDeclaration(name, undefined, undefined, value)
4158
+ ], ts.NodeFlags.Const))
4159
+ }
4160
+ }
4161
+
3912
4162
  function runtimeModuleReference(node) {
3913
4163
  if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
3914
4164
  const clause = node.importClause
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",