@kudzujs/core 0.7.0 → 0.7.2
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 +4 -4
- package/RELEASES.md +49 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +394 -19
- package/package.json +1 -1
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.
|
|
13
|
+
**0.7.2:** React hook normalization. Aliased hooks, direct members such as `React.useState`, and inline `useCallback` now compile to existing Kudzu capabilities without React, a VDOM, or hydration. See [release notes](./RELEASES.md#072---react-hook-normalization).
|
|
14
14
|
|
|
15
15
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
16
16
|
|
|
@@ -74,7 +74,7 @@ export default function Header() {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
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
|
|
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 or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. Inline `useCallback(function, literalDependencies)` is erased to its function because Kudzu does not retain or rerender a browser component. `memo`, `useMemo`, React classes, and side-effect or dynamic React imports remain unsupported. A static route using these import forms still emits zero JavaScript.
|
|
78
78
|
|
|
79
79
|
Create `src/pages/index.tsx`:
|
|
80
80
|
|
|
@@ -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
|
|
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 {
|
|
@@ -225,7 +225,7 @@ const add = (title: string) => dispatch({ type: "add", title })
|
|
|
225
225
|
return <Input onSubmit={add} />
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
Kudzu substitutes the callback into the child's compiled event handler at build time. This is not general function-prop serialization: only one nested specialized callback boundary is supported, and
|
|
228
|
+
Kudzu substitutes the callback into the child's compiled event handler at build time. An inline React `useCallback` wrapper is erased before this analysis. This is not general function-prop serialization: only one nested specialized callback boundary is supported, and further forwarding, effects, component roots, and callback use outside event handlers are rejected.
|
|
229
229
|
|
|
230
230
|
Reducer dispatch and callback components may use destructured string, finite-number, boolean, or `null` defaults. A missing prop is replaced during specialization; object, array, computed, and function-call defaults remain unsupported:
|
|
231
231
|
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.2 - React hook normalization
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.2 accepts more ordinary React/Vite hook syntax while compiling through the existing static HTML and direct DOM capability paths.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.2
|
|
8
|
+
|
|
9
|
+
- Supported hooks imported from `react` may retain aliases such as `useState as useMenuState` and `useEffect as runEffect`.
|
|
10
|
+
- Default and namespace imports may use direct supported members such as `React.useState(...)`.
|
|
11
|
+
- Inline React `useCallback` wrappers with literal inert dependencies are erased to their callback before Kudzu specialization.
|
|
12
|
+
- Captured local state must appear in the callback dependency array, preserving an actionable boundary around stale React closures.
|
|
13
|
+
- Unsupported members, indirect hook references, computed React members, and effectful dependency expressions fail with source locations.
|
|
14
|
+
- Type-only React namespaces remain available to TypeScript and do not trigger runtime migration diagnostics.
|
|
15
|
+
- A React/Vite-shaped app fixture verifies CSS, SVG assets, a non-root base, aliased state/effects, member state, repeated callback updates, browser interaction, and a zero-JavaScript static route.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
`useCallback` requires an inline function and a literal array containing only identifiers or primitive literals. `memo`, `useMemo`, React classes, indirect hook references, side-effect React imports, and dynamic React imports remain unsupported. React, a VDOM, hydration, and a retained browser component tree are never emitted.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 0.7.1 - Vite-style landing assets
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
### New in 0.7.1
|
|
32
|
+
|
|
33
|
+
- Relative side-effect CSS imports are validated and erased before server module evaluation.
|
|
34
|
+
- Default CSS Module imports compile to deterministic scoped class maps with no client runtime.
|
|
35
|
+
- Relative image, SVG, and font imports compile to base-aware URL strings; supported assets also accept `?url`.
|
|
36
|
+
- Relative CSS `url(...)` references are rewritten to base-aware emitted URLs, preserving query and hash suffixes.
|
|
37
|
+
- Referenced asset bytes are copied under deterministic source-relative `dist/assets` paths.
|
|
38
|
+
- Static assets and CSS Modules work inside specialized relative keyed-row components.
|
|
39
|
+
- Declaration files under `src` remain available to TypeScript but are excluded from executable module compilation.
|
|
40
|
+
- The migration fixture verifies a non-root base, byte-identical assets, scoped classes, browser interaction, and zero JavaScript on its static route.
|
|
41
|
+
|
|
42
|
+
### Boundary
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
### Upgrade
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install @kudzujs/core@^0.7.1
|
|
50
|
+
```
|
|
51
|
+
|
|
3
52
|
## 0.7.0 - React-source migration preview
|
|
4
53
|
|
|
5
54
|
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.
|
package/framework/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
5
|
+
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, inline `useCallback`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. `useCallback` is erased to its function because no browser component rerender occurs. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
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.
|
|
8
8
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
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
|
|
|
@@ -1729,10 +1744,133 @@ function hasReactModuleReference(source, file) {
|
|
|
1729
1744
|
return found
|
|
1730
1745
|
}
|
|
1731
1746
|
|
|
1732
|
-
function
|
|
1747
|
+
function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
1748
|
+
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1749
|
+
const aliases = new Map()
|
|
1750
|
+
const reactObjects = new Set()
|
|
1751
|
+
for (const statement of sourceFile.statements) {
|
|
1752
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react") continue
|
|
1753
|
+
if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
|
|
1754
|
+
const bindings = statement.importClause?.namedBindings
|
|
1755
|
+
if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
|
|
1756
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
|
|
1757
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
1758
|
+
if (!entry.isTypeOnly && (supported.has(imported) || imported === "useCallback")) aliases.set(entry.name.text, imported)
|
|
1759
|
+
else if (!entry.isTypeOnly && (imported === "memo" || /^use[A-Z]/.test(imported))) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (!aliases.size && !reactObjects.size) return sourceFile
|
|
1763
|
+
|
|
1764
|
+
const migrationCallName = call => {
|
|
1765
|
+
if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
|
|
1766
|
+
if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression) && reactObjects.has(call.expression.expression.text) && !isShadowedIdentifier(call.expression.expression, sourceFile)) return call.expression.name.text
|
|
1767
|
+
return undefined
|
|
1768
|
+
}
|
|
1769
|
+
const ownerStateNames = owner => {
|
|
1770
|
+
const names = new Set()
|
|
1771
|
+
const collect = node => {
|
|
1772
|
+
if (node !== owner && isFunctionLike(node)) return
|
|
1773
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ["useReducer", "useState"].includes(migrationCallName(node.initializer))) {
|
|
1774
|
+
const state = node.name.elements[0]
|
|
1775
|
+
if (state && ts.isBindingElement(state) && ts.isIdentifier(state.name)) names.add(state.name.text)
|
|
1776
|
+
}
|
|
1777
|
+
ts.forEachChild(node, collect)
|
|
1778
|
+
}
|
|
1779
|
+
collect(owner)
|
|
1780
|
+
return names
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const validate = node => {
|
|
1784
|
+
if (ts.isTypeNode(node)) return
|
|
1785
|
+
if (ts.isIdentifier(node) && aliases.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, `Aliased React ${aliases.get(node.text)} must be called directly`)
|
|
1786
|
+
if (ts.isIdentifier(node) && reactObjects.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "React default or namespace imports may only be used for direct supported members or React.Fragment")
|
|
1787
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
1788
|
+
const name = node.name.text
|
|
1789
|
+
if (name !== "Fragment" && !(ts.isCallExpression(node.parent) && node.parent.expression === node && (supported.has(name) || name === "useCallback"))) throw sourceNodeError(node, sourceFile, `React.${name} is not supported; use a directly supported hook call or React.Fragment`)
|
|
1790
|
+
}
|
|
1791
|
+
ts.forEachChild(node, validate)
|
|
1792
|
+
}
|
|
1793
|
+
validate(sourceFile)
|
|
1794
|
+
|
|
1795
|
+
const required = new Set()
|
|
1796
|
+
const imported = new Set()
|
|
1797
|
+
const visitor = node => {
|
|
1798
|
+
if (ts.isCallExpression(node)) {
|
|
1799
|
+
const name = migrationCallName(node)
|
|
1800
|
+
if (name === "useCallback") {
|
|
1801
|
+
if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useCallback() requires an inline function and a literal dependency array")
|
|
1802
|
+
const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
|
|
1803
|
+
if (dependency) throw sourceNodeError(dependency, sourceFile, "React useCallback() dependencies must be identifiers or primitive literals")
|
|
1804
|
+
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
1805
|
+
const owner = nearestFunction(node)
|
|
1806
|
+
const stale = owner && [...ownerStateNames(owner)].find(state => referenceIdentifiers(node.arguments[0], state).length && !dependencies.has(state))
|
|
1807
|
+
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1808
|
+
return ts.visitNode(node.arguments[0], visitor)
|
|
1809
|
+
}
|
|
1810
|
+
if (name && supported.has(name)) {
|
|
1811
|
+
required.add(name)
|
|
1812
|
+
return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
|
|
1816
|
+
const clause = node.importClause
|
|
1817
|
+
if (!clause) return node
|
|
1818
|
+
let bindings = clause.namedBindings
|
|
1819
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
1820
|
+
const entries = []
|
|
1821
|
+
for (const entry of bindings.elements) {
|
|
1822
|
+
const name = (entry.propertyName ?? entry.name).text
|
|
1823
|
+
if (!entry.isTypeOnly && name === "useCallback") continue
|
|
1824
|
+
if (!entry.isTypeOnly && supported.has(name)) {
|
|
1825
|
+
if (imported.has(name)) continue
|
|
1826
|
+
imported.add(name)
|
|
1827
|
+
required.add(name)
|
|
1828
|
+
entries.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
|
|
1829
|
+
} else {
|
|
1830
|
+
entries.push(entry)
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
bindings = entries.length ? factory.updateNamedImports(bindings, entries) : undefined
|
|
1834
|
+
}
|
|
1835
|
+
if (!clause.name && !bindings) return undefined
|
|
1836
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, clause.name, bindings), node.moduleSpecifier, node.attributes)
|
|
1837
|
+
}
|
|
1838
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1839
|
+
}
|
|
1840
|
+
let normalized = ts.visitNode(sourceFile, visitor)
|
|
1841
|
+
const missing = [...required].filter(name => !imported.has(name)).sort()
|
|
1842
|
+
if (!missing.length) return normalized
|
|
1843
|
+
for (const name of missing) {
|
|
1844
|
+
const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
|
|
1845
|
+
if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
|
|
1846
|
+
}
|
|
1847
|
+
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
|
|
1848
|
+
const statements = [...normalized.statements]
|
|
1849
|
+
const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
|
|
1850
|
+
statements.splice(lastImport + 1, 0, declaration)
|
|
1851
|
+
normalized = factory.updateSourceFile(normalized, statements)
|
|
1852
|
+
return normalized
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function importDeclarationNames(statement) {
|
|
1856
|
+
const names = []
|
|
1857
|
+
if (statement.importClause?.name) names.push(statement.importClause.name.text)
|
|
1858
|
+
const bindings = statement.importClause?.namedBindings
|
|
1859
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
|
|
1860
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) names.push(entry.name.text)
|
|
1861
|
+
return names
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function isReactCallbackDependency(node) {
|
|
1865
|
+
node = unwrapExpression(node)
|
|
1866
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
|
|
1733
1870
|
return context => sourceFile => {
|
|
1734
1871
|
const factory = context.factory
|
|
1735
1872
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
1873
|
+
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
1736
1874
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
1737
1875
|
ts.setParentRecursive(sourceFile, false)
|
|
1738
1876
|
rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
|
|
@@ -1742,7 +1880,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1742
1880
|
const importedSource = target => {
|
|
1743
1881
|
let imported = importedSources.get(target)
|
|
1744
1882
|
if (!imported) {
|
|
1745
|
-
imported =
|
|
1883
|
+
imported = normalizeReactMigrationSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
1884
|
+
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
1746
1885
|
ts.setParentRecursive(imported, false)
|
|
1747
1886
|
importedSources.set(target, imported)
|
|
1748
1887
|
}
|
|
@@ -1901,10 +2040,23 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1901
2040
|
usesRowState ||= specialization.rowStates.length > 0
|
|
1902
2041
|
usesRowRef ||= specialization.rowRefs.length > 0
|
|
1903
2042
|
}
|
|
1904
|
-
const mergeSpecializedImports = (root, componentSource, call) => {
|
|
2043
|
+
const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
|
|
1905
2044
|
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1906
2045
|
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
2046
|
const substitutions = new Map()
|
|
2047
|
+
for (const statement of componentSource.statements) {
|
|
2048
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !isStaticImport(statement.moduleSpecifier.text)) continue
|
|
2049
|
+
const entry = staticImportEntry(statement, componentSource, componentSource.fileName, staticFiles, importedAssets, cssModules, base, factory)
|
|
2050
|
+
if (!entry?.name) continue
|
|
2051
|
+
if (referenceIdentifiers(root, entry.name).length) substitutions.set(entry.name, entry.value)
|
|
2052
|
+
for (const effect of effects) {
|
|
2053
|
+
if (effect.source.getSourceFile() !== componentSource) continue
|
|
2054
|
+
if (!referenceIdentifiers(effect.call, entry.name).length) continue
|
|
2055
|
+
ts.setParentRecursive(effect.call, false)
|
|
2056
|
+
effect.call = substituteClone(effect.call, new Map([[entry.name, entry.value]]), factory, context)
|
|
2057
|
+
synthesizeTree(effect.call)
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
1908
2060
|
for (const [name, entry] of componentImports) {
|
|
1909
2061
|
const references = referenceIdentifiers(root, name)
|
|
1910
2062
|
if (!references.length) continue
|
|
@@ -1939,7 +2091,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1939
2091
|
for (const nestedCall of nestedCalls) {
|
|
1940
2092
|
const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
|
|
1941
2093
|
if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
|
|
1942
|
-
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall)
|
|
2094
|
+
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
|
|
1943
2095
|
synthesizeTree(nested.root)
|
|
1944
2096
|
replacements.set(nestedCall, nested.root)
|
|
1945
2097
|
count++
|
|
@@ -2022,7 +2174,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2022
2174
|
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
2023
2175
|
registerRowHooks(call, specialization)
|
|
2024
2176
|
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
2025
|
-
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
|
|
2177
|
+
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
|
|
2026
2178
|
synthesizeTree(specialization.root)
|
|
2027
2179
|
componentSpecializations.set(call, specialization)
|
|
2028
2180
|
reducerComponentCalls.add(call)
|
|
@@ -2119,7 +2271,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2119
2271
|
const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
|
|
2120
2272
|
registerRowHooks(node, specialization)
|
|
2121
2273
|
specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
|
|
2122
|
-
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node))
|
|
2274
|
+
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
|
|
2123
2275
|
expandedRowSpecializations.set(specialization.root, specialization)
|
|
2124
2276
|
if (currentAggregate) {
|
|
2125
2277
|
currentAggregate.effects.push(...specialization.effects)
|
|
@@ -2164,7 +2316,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2164
2316
|
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
|
|
2165
2317
|
const componentSource = specialization.componentSource ?? sourceFile
|
|
2166
2318
|
specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
|
|
2167
|
-
if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root))
|
|
2319
|
+
if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
|
|
2168
2320
|
if (specialization.root !== originalParts.root) componentSpecializations.set(originalParts.root, specialization)
|
|
2169
2321
|
const root = specialization.root
|
|
2170
2322
|
let callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
|
|
@@ -2228,6 +2380,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2228
2380
|
}
|
|
2229
2381
|
|
|
2230
2382
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
2383
|
+
if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
|
|
2231
2384
|
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
2232
2385
|
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
2233
2386
|
}
|
|
@@ -2757,7 +2910,7 @@ function jsxCallHasReducerCallbackProp(call, reducers) {
|
|
|
2757
2910
|
function runtimeImportNames(sourceFile, relative) {
|
|
2758
2911
|
const names = new Set()
|
|
2759
2912
|
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
|
|
2913
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative || isStaticImport(statement.moduleSpecifier.text)) continue
|
|
2761
2914
|
const clause = statement.importClause
|
|
2762
2915
|
if (clause.name) names.add(clause.name.text)
|
|
2763
2916
|
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
|
|
@@ -3587,6 +3740,7 @@ function isShadowedIdentifier(node, scopeRoot) {
|
|
|
3587
3740
|
if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
|
|
3588
3741
|
for (let current = node.parent; current; current = current.parent) {
|
|
3589
3742
|
if (current === scopeRoot) break
|
|
3743
|
+
if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
|
|
3590
3744
|
if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
|
|
3591
3745
|
if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
|
|
3592
3746
|
if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
|
|
@@ -3662,7 +3816,7 @@ function reducersForNode(node, reducersByFunction) {
|
|
|
3662
3816
|
function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
3663
3817
|
const bindings = new Map()
|
|
3664
3818
|
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
|
|
3819
|
+
if (!ts.isImportDeclaration(node) || !node.importClause || node.importClause.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !node.moduleSpecifier.text.startsWith(".") || isStaticImport(node.moduleSpecifier.text)) continue
|
|
3666
3820
|
let target
|
|
3667
3821
|
try {
|
|
3668
3822
|
target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
@@ -3857,6 +4011,7 @@ async function collectClientModules(entries, sourceFiles) {
|
|
|
3857
4011
|
for (const node of sourceFile.statements) {
|
|
3858
4012
|
if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !runtimeModuleReference(node)) continue
|
|
3859
4013
|
if (!node.moduleSpecifier.text.startsWith(".")) throw new Error(`${relative(root, file)} Imported client helpers may only use relative runtime imports`)
|
|
4014
|
+
if (isStaticImport(node.moduleSpecifier.text)) continue
|
|
3860
4015
|
queue.push(resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles))
|
|
3861
4016
|
}
|
|
3862
4017
|
}
|
|
@@ -3869,12 +4024,13 @@ async function collectClientModules(entries, sourceFiles) {
|
|
|
3869
4024
|
return [...modules].sort()
|
|
3870
4025
|
}
|
|
3871
4026
|
|
|
3872
|
-
async function compileClientModule(file, sourceFiles) {
|
|
4027
|
+
async function compileClientModule(file, sourceFiles, staticFiles, importedAssets, cssModules, base) {
|
|
3873
4028
|
const source = await readFile(file, "utf8")
|
|
3874
4029
|
const transformer = context => sourceFile => {
|
|
3875
4030
|
const factory = context.factory
|
|
3876
4031
|
const visitor = node => {
|
|
3877
4032
|
if (ts.isImportDeclaration(node) && runtimeModuleReference(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
4033
|
+
if (isStaticImport(node.moduleSpecifier.text)) return staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory)?.replacement
|
|
3878
4034
|
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
3879
4035
|
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(clientModulePath(file), clientModulePath(target))), node.attributes)
|
|
3880
4036
|
}
|
|
@@ -3909,6 +4065,225 @@ function resolveSourceImport(importer, specifier, sourceFiles) {
|
|
|
3909
4065
|
return matches[0]
|
|
3910
4066
|
}
|
|
3911
4067
|
|
|
4068
|
+
function staticImportExtension(specifier) {
|
|
4069
|
+
return extname(specifier.split(/[?#]/, 1)[0]).toLowerCase()
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
function isStaticImport(specifier) {
|
|
4073
|
+
const extension = staticImportExtension(specifier)
|
|
4074
|
+
return extension === ".css" || staticAssetExtensions.has(extension)
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function resolveStaticImport(importer, specifier, staticFiles) {
|
|
4078
|
+
const target = resolve(dirname(importer), specifier.split(/[?#]/, 1)[0])
|
|
4079
|
+
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/`)
|
|
4080
|
+
return target
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
async function safeStaticFiles(files) {
|
|
4084
|
+
const sourceRoot = await realpath(sourceDirectory)
|
|
4085
|
+
const entries = await Promise.all(files.map(async file => {
|
|
4086
|
+
try {
|
|
4087
|
+
const target = await realpath(file)
|
|
4088
|
+
const path = relative(sourceRoot, target)
|
|
4089
|
+
if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path) || !(await stat(target)).isFile()) return undefined
|
|
4090
|
+
return file
|
|
4091
|
+
} catch {
|
|
4092
|
+
return undefined
|
|
4093
|
+
}
|
|
4094
|
+
}))
|
|
4095
|
+
return new Set(entries.filter(Boolean))
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
4099
|
+
const ordered = []
|
|
4100
|
+
const seenStyles = new Set()
|
|
4101
|
+
const seenSources = new Set()
|
|
4102
|
+
const sourceSet = new Set(sourceFiles)
|
|
4103
|
+
const visit = file => {
|
|
4104
|
+
if (seenSources.has(file)) return
|
|
4105
|
+
seenSources.add(file)
|
|
4106
|
+
const sourceFile = parseSourceFile(file, sourceIndex.get(file))
|
|
4107
|
+
for (const statement of sourceFile.statements) {
|
|
4108
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
|
|
4109
|
+
const specifier = statement.moduleSpecifier.text
|
|
4110
|
+
if (staticImportExtension(specifier) === ".css") {
|
|
4111
|
+
let target
|
|
4112
|
+
try { target = resolveStaticImport(file, specifier, staticFiles) } catch { continue }
|
|
4113
|
+
if (!seenStyles.has(target)) {
|
|
4114
|
+
seenStyles.add(target)
|
|
4115
|
+
ordered.push(target)
|
|
4116
|
+
}
|
|
4117
|
+
continue
|
|
4118
|
+
}
|
|
4119
|
+
if (isStaticImport(specifier)) continue
|
|
4120
|
+
try { visit(resolveSourceImport(file, specifier, sourceSet)) } catch {}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
for (const file of sourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))) visit(file)
|
|
4124
|
+
for (const file of sourceFiles) visit(file)
|
|
4125
|
+
return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base) {
|
|
4129
|
+
const cssModules = new Map()
|
|
4130
|
+
const cssOutputs = new Map()
|
|
4131
|
+
for (const file of cssFiles) {
|
|
4132
|
+
let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base)
|
|
4133
|
+
if (file.toLowerCase().endsWith(".module.css")) {
|
|
4134
|
+
if (/\bcomposes\s*:/i.test(maskCssCommentsAndStrings(css))) throw new Error(`${relative(root, file)} CSS Modules composes is not supported`)
|
|
4135
|
+
const prefix = `k${createHash("sha256").update(relative(sourceDirectory, file).replaceAll(sep, "/")).digest("hex").slice(0, 8)}`
|
|
4136
|
+
css = (await transform(css, { loader: "local-css", sourcefile: `${prefix}.css`, target: "es2022" })).code
|
|
4137
|
+
const classes = {}
|
|
4138
|
+
for (const match of css.matchAll(new RegExp(`\\.${prefix}_([_a-zA-Z][_a-zA-Z0-9-]*)`, "g"))) classes[match[1]] = match[0].slice(1)
|
|
4139
|
+
cssModules.set(file, classes)
|
|
4140
|
+
}
|
|
4141
|
+
cssOutputs.set(file, css)
|
|
4142
|
+
}
|
|
4143
|
+
return { cssModules, cssOutputs }
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
|
|
4147
|
+
let output = ""
|
|
4148
|
+
let cursor = 0
|
|
4149
|
+
let index = 0
|
|
4150
|
+
while (index < css.length) {
|
|
4151
|
+
if (css.startsWith("/*", index)) {
|
|
4152
|
+
index = css.indexOf("*/", index + 2)
|
|
4153
|
+
index = index === -1 ? css.length : index + 2
|
|
4154
|
+
continue
|
|
4155
|
+
}
|
|
4156
|
+
if (css[index] === '"' || css[index] === "'") {
|
|
4157
|
+
index = cssStringEnd(css, index)
|
|
4158
|
+
continue
|
|
4159
|
+
}
|
|
4160
|
+
if (css.slice(index, index + 3).toLowerCase() !== "url" || /[-_a-z\d]/i.test(css[index - 1] ?? "")) {
|
|
4161
|
+
index++
|
|
4162
|
+
continue
|
|
4163
|
+
}
|
|
4164
|
+
let open = index + 3
|
|
4165
|
+
while (/\s/.test(css[open] ?? "")) open++
|
|
4166
|
+
if (css[open] !== "(") {
|
|
4167
|
+
index++
|
|
4168
|
+
continue
|
|
4169
|
+
}
|
|
4170
|
+
let start = open + 1
|
|
4171
|
+
while (/\s/.test(css[start] ?? "")) start++
|
|
4172
|
+
const quote = css[start] === '"' || css[start] === "'" ? css[start] : ""
|
|
4173
|
+
const valueStart = quote ? start + 1 : start
|
|
4174
|
+
let end = valueStart
|
|
4175
|
+
if (quote) {
|
|
4176
|
+
end = cssStringEnd(css, start) - 1
|
|
4177
|
+
if (css[end] !== quote) {
|
|
4178
|
+
index = open + 1
|
|
4179
|
+
continue
|
|
4180
|
+
}
|
|
4181
|
+
} else {
|
|
4182
|
+
while (end < css.length && css[end] !== ")") end += css[end] === "\\" ? 2 : 1
|
|
4183
|
+
}
|
|
4184
|
+
let close = quote ? end + 1 : end
|
|
4185
|
+
while (/\s/.test(css[close] ?? "")) close++
|
|
4186
|
+
if (css[close] !== ")") {
|
|
4187
|
+
index = open + 1
|
|
4188
|
+
continue
|
|
4189
|
+
}
|
|
4190
|
+
const value = css.slice(valueStart, end).trim()
|
|
4191
|
+
const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base)
|
|
4192
|
+
output += css.slice(cursor, index) + (replacement ?? css.slice(index, close + 1))
|
|
4193
|
+
cursor = close + 1
|
|
4194
|
+
index = close + 1
|
|
4195
|
+
}
|
|
4196
|
+
return output + css.slice(cursor)
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base) {
|
|
4200
|
+
if (!value || value.startsWith("/") || value.startsWith("#") || value.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(value)) return undefined
|
|
4201
|
+
const split = value.search(/[?#]/)
|
|
4202
|
+
const pathname = split === -1 ? value : value.slice(0, split)
|
|
4203
|
+
const suffix = split === -1 ? "" : value.slice(split)
|
|
4204
|
+
const target = resolve(dirname(file), pathname)
|
|
4205
|
+
if (!staticFiles.has(target)) throw new Error(`${relative(root, file)} CSS URL ${JSON.stringify(value)} must resolve to an existing regular file under src/`)
|
|
4206
|
+
importedAssets.add(target)
|
|
4207
|
+
const url = assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`)
|
|
4208
|
+
return `url(${quote || '"'}${url}${suffix}${quote || '"'})`
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
function cssStringEnd(css, start) {
|
|
4212
|
+
const quote = css[start]
|
|
4213
|
+
let index = start + 1
|
|
4214
|
+
while (index < css.length) {
|
|
4215
|
+
if (css[index] === "\\") index += 2
|
|
4216
|
+
else if (css[index++] === quote) break
|
|
4217
|
+
else if (css[index - 1] === "\n") break
|
|
4218
|
+
}
|
|
4219
|
+
return index
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
function maskCssCommentsAndStrings(css) {
|
|
4223
|
+
const masked = [...css]
|
|
4224
|
+
let index = 0
|
|
4225
|
+
while (index < css.length) {
|
|
4226
|
+
let end
|
|
4227
|
+
if (css.startsWith("/*", index)) {
|
|
4228
|
+
const close = css.indexOf("*/", index + 2)
|
|
4229
|
+
end = close === -1 ? css.length : close + 2
|
|
4230
|
+
} else if (css[index] === '"' || css[index] === "'") {
|
|
4231
|
+
end = cssStringEnd(css, index)
|
|
4232
|
+
} else {
|
|
4233
|
+
index++
|
|
4234
|
+
continue
|
|
4235
|
+
}
|
|
4236
|
+
for (; index < end; index++) if (masked[index] !== "\n") masked[index] = " "
|
|
4237
|
+
}
|
|
4238
|
+
return masked.join("")
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
|
|
4242
|
+
const specifier = node.moduleSpecifier.text
|
|
4243
|
+
if (specifier.includes("\\") || specifier.includes("#")) throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports require forward-slash paths without hash suffixes")
|
|
4244
|
+
const queryIndex = specifier.indexOf("?")
|
|
4245
|
+
const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
|
|
4246
|
+
if (query && query !== "url") throw sourceNodeError(node.moduleSpecifier, sourceFile, "Static asset imports support only the ?url query")
|
|
4247
|
+
let target
|
|
4248
|
+
try {
|
|
4249
|
+
target = resolveStaticImport(file, specifier, staticFiles)
|
|
4250
|
+
} catch (error) {
|
|
4251
|
+
throw sourceNodeError(node.moduleSpecifier, sourceFile, error.message)
|
|
4252
|
+
}
|
|
4253
|
+
if (node.attributes) throw sourceNodeError(node.attributes, sourceFile, "Static asset import attributes are not supported")
|
|
4254
|
+
const extension = staticImportExtension(specifier)
|
|
4255
|
+
if (query === "url") {
|
|
4256
|
+
if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
|
|
4257
|
+
if (extension !== ".css") importedAssets.add(target)
|
|
4258
|
+
const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
|
|
4259
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
4260
|
+
}
|
|
4261
|
+
if (extension === ".css") {
|
|
4262
|
+
const classes = cssModules.get(target)
|
|
4263
|
+
if (!node.importClause) return undefined
|
|
4264
|
+
if (!classes || !node.importClause.name || node.importClause.isTypeOnly || node.importClause.namedBindings) {
|
|
4265
|
+
const message = classes ? "CSS Modules require one default import" : "CSS imports must be side-effect imports"
|
|
4266
|
+
throw sourceNodeError(node.importClause, sourceFile, message)
|
|
4267
|
+
}
|
|
4268
|
+
const value = factory.createObjectLiteralExpression(Object.entries(classes).sort(([left], [right]) => left.localeCompare(right)).map(([name, scoped]) => factory.createPropertyAssignment(factory.createStringLiteral(name), factory.createStringLiteral(scoped))))
|
|
4269
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
4270
|
+
}
|
|
4271
|
+
if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
|
|
4272
|
+
importedAssets.add(target)
|
|
4273
|
+
const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
|
|
4274
|
+
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
function staticImportReplacement(name, value, factory) {
|
|
4278
|
+
return {
|
|
4279
|
+
name,
|
|
4280
|
+
value,
|
|
4281
|
+
replacement: factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
|
|
4282
|
+
factory.createVariableDeclaration(name, undefined, undefined, value)
|
|
4283
|
+
], ts.NodeFlags.Const))
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
|
|
3912
4287
|
function runtimeModuleReference(node) {
|
|
3913
4288
|
if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
|
|
3914
4289
|
const clause = node.importClause
|