@kudzujs/core 0.6.28 → 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 +20 -2
- package/RELEASES.md +69 -0
- package/framework/README.md +2 -0
- package/framework/build.mjs +288 -19
- package/framework/core.d.ts +4 -0
- package/framework/core.mjs +3 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ HTML-first TSX framework with synchronous state semantics and no virtual DOM.
|
|
|
8
8
|
|
|
9
9
|
Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal source restructuring. It keeps familiar function components, props, children, collection rendering, conditions, event handlers, `useState`, reduced `useReducer`, refs, and effects, preferring compiler specialization over imperative DOM rewrites. This is a general migration model, not compatibility for one application. Static components compile to HTML; interactions compile to direct DOM capabilities and external ESM only where used.
|
|
10
10
|
|
|
11
|
-
> Experimental `0.
|
|
11
|
+
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
|
+
|
|
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).
|
|
12
14
|
|
|
13
15
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
14
16
|
|
|
@@ -58,6 +60,22 @@ Configure TypeScript:
|
|
|
58
60
|
|
|
59
61
|
Make sure application TSX files are included by this `tsconfig.json`. Files outside its `include` may fall into an editor-inferred React project and incorrectly report a missing `react/jsx-runtime` or React event-type errors.
|
|
60
62
|
|
|
63
|
+
Existing React migration source may retain conventional imports while components are moved under `src`:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import React, { useState } from "react"
|
|
67
|
+
|
|
68
|
+
export default function Header() {
|
|
69
|
+
const [open, setOpen] = useState(false)
|
|
70
|
+
return <React.Fragment>
|
|
71
|
+
<button onClick={() => setOpen(!open)}>{open ? "Close" : "Menu"}</button>
|
|
72
|
+
{open && <nav>Navigation</nav>}
|
|
73
|
+
</React.Fragment>
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports must retain those names. Default and namespace React imports currently support `Fragment`; named `Fragment` also works. Aliased hooks, `React.useState`, `memo`, `useMemo`, `useCallback`, React classes, and side-effect or dynamic React imports remain unsupported. A static route using these import forms still emits zero JavaScript.
|
|
78
|
+
|
|
61
79
|
Create `src/pages/index.tsx`:
|
|
62
80
|
|
|
63
81
|
```tsx
|
|
@@ -128,7 +146,7 @@ Static trusted HTML can be rendered without a transform layer:
|
|
|
128
146
|
|
|
129
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.
|
|
130
148
|
|
|
131
|
-
Every CSS file under `src` is
|
|
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:
|
|
132
150
|
|
|
133
151
|
```js
|
|
134
152
|
export default {
|
package/RELEASES.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Kudzu Releases
|
|
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
|
+
|
|
28
|
+
## 0.7.0 - React-source migration preview
|
|
29
|
+
|
|
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.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import React, { useState } from "react"
|
|
34
|
+
|
|
35
|
+
export default function Header() {
|
|
36
|
+
const [open, setOpen] = useState(false)
|
|
37
|
+
|
|
38
|
+
return <React.Fragment>
|
|
39
|
+
<button onClick={() => setOpen(!open)}>{open ? "Close" : "Menu"}</button>
|
|
40
|
+
{open && <nav>Navigation</nav>}
|
|
41
|
+
</React.Fragment>
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### New in 0.7.0
|
|
46
|
+
|
|
47
|
+
- Conventional unaliased named imports of supported hooks from `react` compile through Kudzu without loading React.
|
|
48
|
+
- Default, namespace, and named `Fragment` imports are accepted for migration source.
|
|
49
|
+
- Relative function components, props, children, conditions, attributes, text, and event handlers keep their familiar TSX shape.
|
|
50
|
+
- Static routes using the accepted React import forms still ship zero JavaScript.
|
|
51
|
+
- Interactive routes ship direct DOM capabilities only; the landing-page acceptance fixture adds state, text, attribute, condition, and menu-handler capabilities.
|
|
52
|
+
- Emitted modules are checked for surviving runtime React references, and side-effect React imports fail with a source location.
|
|
53
|
+
- Keyed collections now support analyzable `filter`, direct-property `flatMap`, `Array.from`, positional keys, recursively deep sibling child maps, nested conditions, latest-item handlers, multiple serializable row states, effects, and `null` object refs.
|
|
54
|
+
|
|
55
|
+
### Current boundary
|
|
56
|
+
|
|
57
|
+
This is source migration support, not a React compatibility runtime. Aliased hooks, member hook calls such as `React.useState`, `memo`, `useMemo`, `useCallback`, React classes, React Router, Next-specific components, React UI packages, side-effect imports, and dynamic React imports remain unsupported. Migrate a real route, reduce the first unsupported pattern to a fixture, and extend the compiler one proven blocker at a time.
|
|
58
|
+
|
|
59
|
+
### Measured fixture
|
|
60
|
+
|
|
61
|
+
The two-route landing fixture retains React imports across relative components. Its static route has no script, while its interactive mobile-menu route emits 10,245 B raw / 5,030 B aggregate gzip JavaScript across seven capability files. Seven clean builds after one warm-up measured a 310.0 ms median on the development machine described in `MIGRATION_ROADMAP.md`.
|
|
62
|
+
|
|
63
|
+
### Upgrade
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @kudzujs/core@^0.7.0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
New Kudzu source should continue importing APIs from `@kudzujs/core`. Retaining `react` imports is intended for migration input where minimizing source edits matters.
|
package/framework/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
|
|
4
4
|
|
|
5
|
+
Migration source may retain conventional `react` imports for unaliased supported named hooks and default, namespace, or named `Fragment`. `build.mjs` rewrites those module references to `@kudzujs/core` before build-time evaluation. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports. Member hook calls and aliased hooks are deliberately not inferred yet.
|
|
6
|
+
|
|
5
7
|
- `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
|
|
6
8
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
|
7
9
|
- `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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('"', """).replaceAll("'", "'")
|
|
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
|
|
|
@@ -1694,6 +1709,7 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
|
|
|
1694
1709
|
if (errors.length) {
|
|
1695
1710
|
throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
1696
1711
|
}
|
|
1712
|
+
if (hasReactModuleReference(result.outputText, file)) throw new Error(`${relative(root, file)} Runtime React module references are not supported`)
|
|
1697
1713
|
|
|
1698
1714
|
const output = compiledPath(file)
|
|
1699
1715
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
@@ -1716,7 +1732,19 @@ async function compile(file, sourceFiles, sourceIndex, base, workerReferences) {
|
|
|
1716
1732
|
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
|
|
1717
1733
|
}
|
|
1718
1734
|
|
|
1719
|
-
function
|
|
1735
|
+
function hasReactModuleReference(source, file) {
|
|
1736
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
|
|
1737
|
+
let found = false
|
|
1738
|
+
const visit = node => {
|
|
1739
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") found = true
|
|
1740
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text === "react") found = true
|
|
1741
|
+
if (!found) ts.forEachChild(node, visit)
|
|
1742
|
+
}
|
|
1743
|
+
visit(sourceFile)
|
|
1744
|
+
return found
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
|
|
1720
1748
|
return context => sourceFile => {
|
|
1721
1749
|
const factory = context.factory
|
|
1722
1750
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
@@ -1724,7 +1752,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1724
1752
|
ts.setParentRecursive(sourceFile, false)
|
|
1725
1753
|
rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
|
|
1726
1754
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
1727
|
-
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) &&
|
|
1755
|
+
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
1728
1756
|
const importedSources = new Map()
|
|
1729
1757
|
const importedSource = target => {
|
|
1730
1758
|
let imported = importedSources.get(target)
|
|
@@ -1888,10 +1916,23 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1888
1916
|
usesRowState ||= specialization.rowStates.length > 0
|
|
1889
1917
|
usesRowRef ||= specialization.rowRefs.length > 0
|
|
1890
1918
|
}
|
|
1891
|
-
const mergeSpecializedImports = (root, componentSource, call) => {
|
|
1919
|
+
const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
|
|
1892
1920
|
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1893
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")
|
|
1894
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
|
+
}
|
|
1895
1936
|
for (const [name, entry] of componentImports) {
|
|
1896
1937
|
const references = referenceIdentifiers(root, name)
|
|
1897
1938
|
if (!references.length) continue
|
|
@@ -1926,7 +1967,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1926
1967
|
for (const nestedCall of nestedCalls) {
|
|
1927
1968
|
const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
|
|
1928
1969
|
if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
|
|
1929
|
-
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall)
|
|
1970
|
+
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
|
|
1930
1971
|
synthesizeTree(nested.root)
|
|
1931
1972
|
replacements.set(nestedCall, nested.root)
|
|
1932
1973
|
count++
|
|
@@ -2009,7 +2050,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2009
2050
|
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
2010
2051
|
registerRowHooks(call, specialization)
|
|
2011
2052
|
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
2012
|
-
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
|
|
2053
|
+
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
|
|
2013
2054
|
synthesizeTree(specialization.root)
|
|
2014
2055
|
componentSpecializations.set(call, specialization)
|
|
2015
2056
|
reducerComponentCalls.add(call)
|
|
@@ -2106,7 +2147,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2106
2147
|
const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
|
|
2107
2148
|
registerRowHooks(node, specialization)
|
|
2108
2149
|
specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
|
|
2109
|
-
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))
|
|
2110
2151
|
expandedRowSpecializations.set(specialization.root, specialization)
|
|
2111
2152
|
if (currentAggregate) {
|
|
2112
2153
|
currentAggregate.effects.push(...specialization.effects)
|
|
@@ -2151,7 +2192,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2151
2192
|
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
|
|
2152
2193
|
const componentSource = specialization.componentSource ?? sourceFile
|
|
2153
2194
|
specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
|
|
2154
|
-
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))
|
|
2155
2196
|
if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
|
|
2156
2197
|
const root = specialization.root
|
|
2157
2198
|
let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
|
|
@@ -2208,7 +2249,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2208
2249
|
fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
|
|
2209
2250
|
}
|
|
2210
2251
|
|
|
2252
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
|
|
2253
|
+
if (!node.importClause) fail(node, "Side-effect React imports are not supported because Kudzu does not load the React runtime")
|
|
2254
|
+
if (node.importClause.isTypeOnly) return node
|
|
2255
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral("@kudzujs/core"), node.attributes)
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2211
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
|
|
2212
2260
|
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
2213
2261
|
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
2214
2262
|
}
|
|
@@ -2738,7 +2786,7 @@ function jsxCallHasReducerCallbackProp(call, reducers) {
|
|
|
2738
2786
|
function runtimeImportNames(sourceFile, relative) {
|
|
2739
2787
|
const names = new Set()
|
|
2740
2788
|
for (const statement of sourceFile.statements) {
|
|
2741
|
-
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
|
|
2742
2790
|
const clause = statement.importClause
|
|
2743
2791
|
if (clause.name) names.add(clause.name.text)
|
|
2744
2792
|
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
|
|
@@ -3643,7 +3691,7 @@ function reducersForNode(node, reducersByFunction) {
|
|
|
3643
3691
|
function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
3644
3692
|
const bindings = new Map()
|
|
3645
3693
|
for (const node of sourceFile.statements) {
|
|
3646
|
-
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
|
|
3647
3695
|
let target
|
|
3648
3696
|
try {
|
|
3649
3697
|
target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
@@ -3838,6 +3886,7 @@ async function collectClientModules(entries, sourceFiles) {
|
|
|
3838
3886
|
for (const node of sourceFile.statements) {
|
|
3839
3887
|
if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
|
|
3840
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
|
|
3841
3890
|
queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
|
|
3842
3891
|
}
|
|
3843
3892
|
}
|
|
@@ -3850,12 +3899,13 @@ async function collectClientModules(entries, sourceFiles) {
|
|
|
3850
3899
|
return [...modules].sort()
|
|
3851
3900
|
}
|
|
3852
3901
|
|
|
3853
|
-
async function compileClientModule(file, sourceFiles) {
|
|
3902
|
+
async function compileClientModule(file, sourceFiles, staticFiles, importedAssets, cssModules, base) {
|
|
3854
3903
|
const source = await readFile(file, "utf8")
|
|
3855
3904
|
const transformer = context => sourceFile => {
|
|
3856
3905
|
const factory = context.factory
|
|
3857
3906
|
const visitor = node => {
|
|
3858
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
|
|
3859
3909
|
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
3860
3910
|
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
|
|
3861
3911
|
}
|
|
@@ -3890,6 +3940,225 @@ function resolveSourceImport(importer, specifier, sourceFiles) {
|
|
|
3890
3940
|
return matches[0]
|
|
3891
3941
|
}
|
|
3892
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
|
+
|
|
3893
4162
|
function runtimeModuleReference(node) {
|
|
3894
4163
|
if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
|
|
3895
4164
|
const clause = node.importClause
|
package/framework/core.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type Reducer<State, Action> = (state: State, action: Action) => State
|
|
|
3
3
|
export type Dispatch<Action> = (action: Action) => void
|
|
4
4
|
export type EffectCleanup = () => void | Promise<void>
|
|
5
5
|
export type EffectDependency = string | number | boolean | null
|
|
6
|
+
export const Fragment: unique symbol
|
|
6
7
|
|
|
7
8
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
8
9
|
export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
|
|
@@ -22,6 +23,9 @@ export interface Context<T> {
|
|
|
22
23
|
export function createContext<T>(defaultValue: T): Context<T>
|
|
23
24
|
export function useContext<T>(context: Context<T>): T
|
|
24
25
|
|
|
26
|
+
declare const React: { Fragment: typeof Fragment }
|
|
27
|
+
export default React
|
|
28
|
+
|
|
25
29
|
export function behavior(commands: Array<["add" | "set" | "log", unknown, unknown]>): unknown
|
|
26
30
|
export function nativeBehavior(module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
27
31
|
export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
package/framework/core.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const contextMarker = Symbol("kudzu.context")
|
|
|
20
20
|
const contextProviderMarker = Symbol("kudzu.contextProvider")
|
|
21
21
|
const routeScopeMarker = Symbol("kudzu.routeScope")
|
|
22
22
|
const noSelectValue = Symbol("kudzu.no-select-value")
|
|
23
|
+
export const Fragment = Symbol.for("kudzu.fragment")
|
|
23
24
|
const svgAttributeAliases = {
|
|
24
25
|
clipRule: "clip-rule",
|
|
25
26
|
colorInterpolation: "color-interpolation",
|
|
@@ -170,6 +171,8 @@ export function useContext(context) {
|
|
|
170
171
|
return context.defaultValue
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
export default { Fragment }
|
|
175
|
+
|
|
173
176
|
export function behavior(commands) {
|
|
174
177
|
return {
|
|
175
178
|
[behaviorMarker]: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudzujs/core",
|
|
3
|
-
"version": "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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"framework/",
|
|
27
27
|
"GOAL_A.md",
|
|
28
28
|
"GOAL_B.md",
|
|
29
|
+
"RELEASES.md",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|