evolit 0.1.0 → 0.1.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 CHANGED
@@ -8,7 +8,7 @@ This repository now contains the first framework MVP:
8
8
  - nested `layout` composition
9
9
  - server rendering through `@litsx/ssr`
10
10
  - a small `evolit` CLI with `init`, `dev`, `build`, and `start`
11
- - on-demand compilation of authored `.litsx` modules through `@litsx/compiler`
11
+ - on-demand compilation of authored `.jsx` modules through `@litsx/compiler`
12
12
  - a starter template for generating new sites
13
13
 
14
14
  ## MVP Scope
@@ -26,14 +26,26 @@ It focuses on the core contract that matters first:
26
26
 
27
27
  Supported authored module extensions:
28
28
 
29
- - `.litsx`
30
- - `.litsx.jsx`
31
29
  - `.js`
32
30
  - `.jsx`
33
31
  - `.ts`
34
32
  - `.tsx`
35
33
  - `.mjs`
36
34
 
35
+ ### Internal imports
36
+
37
+ Imports beginning with `@/` resolve from the application root in server and browser graphs. This
38
+ keeps internal imports stable when modules move between route directories:
39
+
40
+ ```jsx
41
+ import FeatureCard from "@/app/components/feature-card";
42
+ import { formatPrice } from "@/lib/format-price";
43
+ ```
44
+
45
+ Generated applications declare `"@/*": ["./*"]` in `jsconfig.json`, so editors and typechecking
46
+ use the same convention. An explicit `@/*` mapping in `jsconfig.json` or `tsconfig.json` takes
47
+ priority when an application needs a different source root.
48
+
37
49
  ## Commands
38
50
 
39
51
  ```sh
@@ -306,20 +318,20 @@ code. Request values are isolated with async request context and are discarded w
306
318
 
307
319
  ## Route Boundaries
308
320
 
309
- `not-found.litsx` and `error.litsx` are resolved from the current route directory up to `app/`.
310
- The nearest file wins and its output is wrapped by the route layouts. A root `app/not-found.litsx`
321
+ `not-found.jsx` and `error.jsx` are resolved from the current route directory up to `app/`.
322
+ The nearest file wins and its output is wrapped by the route layouts. A root `app/not-found.jsx`
311
323
  also handles unmatched URLs; without one, evolit returns its minimal built-in 404 document.
312
324
 
313
325
  ```js
314
- // app/blog/error.litsx
326
+ // app/blog/error.jsx
315
327
  export default async function BlogError({ error }) {
316
328
  return `<p>Could not load this post: ${error.message}</p>`;
317
329
  }
318
330
  ```
319
331
 
320
- Boundaries always bypass the route response cache. `loading.litsx` is intentionally not supported
332
+ Boundaries always bypass the route response cache. `loading.jsx` is intentionally not supported
321
333
  yet: it requires an end-to-end streaming document transport rather than an HTML-string fallback.
322
- In production, `error.litsx` receives a generic error with an opaque `digest`; the original error
334
+ In production, `error.jsx` receives a generic error with an opaque `digest`; the original error
323
335
  is reported only on the server.
324
336
 
325
337
  ## Route Handlers
@@ -414,7 +426,7 @@ enumerated at build time, so production applications can declare only those exce
414
426
 
415
427
  ```js
416
428
  export default {
417
- clientBoundaries: ["./src/components/dynamic-card.litsx", "@acme/ui/product-card"],
429
+ clientBoundaries: ["./src/components/dynamic-card.jsx", "@acme/ui/product-card"],
418
430
  };
419
431
  ```
420
432
 
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/litsxdev/nextsx.git"
9
+ "url": "git+https://github.com/litsxdev/evolit.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/litsxdev/nextsx/issues"
12
+ "url": "https://github.com/litsxdev/evolit/issues"
13
13
  },
14
- "homepage": "https://github.com/litsxdev/nextsx#readme",
14
+ "homepage": "https://github.com/litsxdev/evolit#readme",
15
15
  "files": [
16
16
  "src",
17
17
  "templates/default/app/**",
18
18
  "templates/default/jsconfig.json",
19
19
  "templates/default/yarnrc.yml.template"
20
20
  ],
21
- "bin": "./src/cli.js",
21
+ "bin": "src/cli.js",
22
22
  "main": "./src/index.js",
23
23
  "exports": {
24
24
  ".": "./src/index.js",
@@ -55,13 +55,13 @@
55
55
  "author": "LitSX Team",
56
56
  "license": "Apache-2.0",
57
57
  "engines": {
58
- "node": ">=20.19.5"
58
+ "node": "^22.18.0 || >=24.11.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "@jridgewell/remapping": "^2.3.5",
62
- "@litsx/compiler": "0.10.0-canary-feat-ssr-20260802205539",
63
- "@litsx/core": "0.17.0-canary-feat-ssr-20260802205539",
64
- "@litsx/ssr": "0.2.0-canary-feat-ssr-20260802205539",
62
+ "@litsx/compiler": "1.0.0-next.8",
63
+ "@litsx/core": "1.0.0-next.5",
64
+ "@litsx/ssr": "1.0.0-next.3",
65
65
  "@litsx/typescript": "^0.9.0",
66
66
  "@rollup/plugin-commonjs": "^29.0.0",
67
67
  "@rollup/plugin-node-resolve": "^16.0.3",
package/src/build.js CHANGED
@@ -344,7 +344,7 @@ export async function buildProject(projectRoot) {
344
344
  if (assetResolver(moduleId) || clientAssets.byPublicPath?.[moduleId]) continue;
345
345
  const importerPath = routeResult.boundaryModule
346
346
  ?? routeResult.route?.page
347
- ?? path.join(projectRoot, "app", "page.litsx");
347
+ ?? path.join(projectRoot, "app", "page.jsx");
348
348
  let sourcePath = null;
349
349
  if (moduleId.startsWith("file:")) {
350
350
  try { sourcePath = fileURLToPath(moduleId); } catch {}
package/src/compiler.js CHANGED
@@ -52,6 +52,175 @@ function shouldCompileModule(filePath) {
52
52
  return MODULE_EXTENSIONS.some((extension) => filePath.endsWith(extension));
53
53
  }
54
54
 
55
+ function containsJsxSyntax(source, filePath) {
56
+ const sourceFile = ts.createSourceFile(
57
+ filePath,
58
+ source,
59
+ ts.ScriptTarget.Latest,
60
+ true,
61
+ ts.ScriptKind.TSX,
62
+ );
63
+ let containsJsx = false;
64
+
65
+ function visit(node) {
66
+ if (
67
+ ts.isJsxElement(node)
68
+ || ts.isJsxSelfClosingElement(node)
69
+ || ts.isJsxFragment(node)
70
+ ) {
71
+ containsJsx = true;
72
+ return;
73
+ }
74
+ ts.forEachChild(node, visit);
75
+ }
76
+
77
+ visit(sourceFile);
78
+ return containsJsx;
79
+ }
80
+
81
+ function containsLitsxComponentMetadata(source, filePath) {
82
+ const sourceFile = ts.createSourceFile(
83
+ filePath,
84
+ source,
85
+ ts.ScriptTarget.Latest,
86
+ true,
87
+ ts.ScriptKind.TSX,
88
+ );
89
+ let containsMetadata = false;
90
+
91
+ function visit(node) {
92
+ if (
93
+ ts.isBinaryExpression(node)
94
+ && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
95
+ && ts.isPropertyAccessExpression(node.left)
96
+ && ts.isIdentifier(node.left.expression)
97
+ && /^[A-Z]/.test(node.left.expression.text)
98
+ && ["elements", "styles", "properties", "shadowRootOptions", "expose", "lightDom"].includes(node.left.name.text)
99
+ ) {
100
+ containsMetadata = true;
101
+ return;
102
+ }
103
+ ts.forEachChild(node, visit);
104
+ }
105
+
106
+ visit(sourceFile);
107
+ return containsMetadata;
108
+ }
109
+
110
+ function isLitsxAuthoredModule(filePath, source) {
111
+ return (
112
+ filePath.endsWith(".jsx")
113
+ || filePath.endsWith(".tsx")
114
+ || containsJsxSyntax(source, filePath)
115
+ || containsLitsxComponentMetadata(source, filePath)
116
+ );
117
+ }
118
+
119
+ function createIdentitySourceMap(source, sourcePath) {
120
+ return JSON.parse(new MagicString(source).generateMap({
121
+ hires: true,
122
+ includeContent: true,
123
+ source: sourcePath.split(path.sep).join("/"),
124
+ }).toString());
125
+ }
126
+
127
+ function collectSideEffectImports(source, sourcePath) {
128
+ const sourceFile = ts.createSourceFile(
129
+ sourcePath,
130
+ source,
131
+ ts.ScriptTarget.Latest,
132
+ true,
133
+ ts.ScriptKind.TSX,
134
+ );
135
+
136
+ return sourceFile.statements
137
+ .filter((statement) => ts.isImportDeclaration(statement) && statement.importClause == null)
138
+ .map((statement) => ({
139
+ source: statement.moduleSpecifier.text,
140
+ statement: source.slice(statement.getStart(sourceFile), statement.end),
141
+ }));
142
+ }
143
+
144
+ function restoreSideEffectImports(source, sourcePath, transformed, sourceMaps) {
145
+ const transformedSpecifiers = new Set(
146
+ [...transformed.code.matchAll(MODULE_SPECIFIER_PATTERN)]
147
+ .map((match) => match[1] ?? match[2])
148
+ .filter(Boolean),
149
+ );
150
+ const missingImports = collectSideEffectImports(source, sourcePath)
151
+ .filter((entry) => !transformedSpecifiers.has(entry.source));
152
+ if (missingImports.length === 0) {
153
+ return transformed;
154
+ }
155
+
156
+ const magicSource = new MagicString(transformed.code);
157
+ magicSource.prepend(`${missingImports.map((entry) => entry.statement).join("\n")}\n`);
158
+ let map = transformed.map ?? null;
159
+
160
+ if (sourceMaps && map) {
161
+ const intermediateSourceId = `${sourcePath.split(path.sep).join("/")}#evolit-side-effects`;
162
+ const restoredMap = magicSource.generateMap({
163
+ hires: true,
164
+ includeContent: false,
165
+ source: intermediateSourceId,
166
+ });
167
+ map = remapping(restoredMap.toString(), (mappedSource) => (
168
+ mappedSource.endsWith("#evolit-side-effects") ? transformed.map : null
169
+ ));
170
+ }
171
+
172
+ return {
173
+ ...transformed,
174
+ code: magicSource.toString(),
175
+ map,
176
+ };
177
+ }
178
+
179
+ async function transformModuleSource(source, {
180
+ sourcePath,
181
+ sourceMaps,
182
+ ssr = false,
183
+ }) {
184
+ if (isLitsxAuthoredModule(sourcePath, source)) {
185
+ const transformed = await transformLitsx(source, {
186
+ filename: sourcePath,
187
+ sourceMaps,
188
+ ssr,
189
+ });
190
+ return restoreSideEffectImports(source, sourcePath, transformed, sourceMaps);
191
+ }
192
+
193
+ if (sourcePath.endsWith(".ts")) {
194
+ const result = ts.transpileModule(source, {
195
+ fileName: sourcePath,
196
+ compilerOptions: {
197
+ inlineSources: sourceMaps,
198
+ module: ts.ModuleKind.ESNext,
199
+ sourceMap: sourceMaps,
200
+ target: ts.ScriptTarget.ESNext,
201
+ },
202
+ });
203
+ const map = sourceMaps && result.sourceMapText
204
+ ? JSON.parse(result.sourceMapText)
205
+ : null;
206
+ if (map) {
207
+ map.sources = [sourcePath.split(path.sep).join("/")];
208
+ map.sourcesContent = [source];
209
+ }
210
+ return {
211
+ code: result.outputText.replace(/\n?\/\/# sourceMappingURL=.*$/u, ""),
212
+ map,
213
+ metadata: {},
214
+ };
215
+ }
216
+
217
+ return {
218
+ code: source,
219
+ map: sourceMaps ? createIdentitySourceMap(source, sourcePath) : null,
220
+ metadata: {},
221
+ };
222
+ }
223
+
55
224
  function isStaticAssetPath(filePath) {
56
225
  return STATIC_ASSET_EXTENSIONS.some((extension) => filePath.endsWith(extension));
57
226
  }
@@ -537,8 +706,24 @@ async function resolveProjectPathAlias(projectRoot, specifier) {
537
706
  return null;
538
707
  }
539
708
 
709
+ async function resolveProjectRootAlias(projectRoot, specifier) {
710
+ if (!specifier.startsWith("@/") || specifier.length === 2) return null;
711
+
712
+ const normalizedProjectRoot = path.resolve(projectRoot);
713
+ const candidatePath = path.resolve(normalizedProjectRoot, specifier.slice(2));
714
+ const relativePath = path.relative(normalizedProjectRoot, candidatePath);
715
+ const isOutsideProject =
716
+ relativePath === ".."
717
+ || relativePath.startsWith(`..${path.sep}`)
718
+ || path.isAbsolute(relativePath);
719
+ if (isOutsideProject) return null;
720
+
721
+ return resolveImportPath(path.join(normalizedProjectRoot, "__alias__.js"), candidatePath);
722
+ }
723
+
540
724
  async function resolveProjectMappedImport(projectRoot, importerPath, specifier) {
541
725
  return await resolveProjectPathAlias(projectRoot, specifier)
726
+ ?? await resolveProjectRootAlias(projectRoot, specifier)
542
727
  ?? await resolveProjectPackageImportMap(importerPath, specifier);
543
728
  }
544
729
 
@@ -938,8 +1123,9 @@ async function compileModuleGraphUncached(entryPath, options = {}) {
938
1123
 
939
1124
  await ensureDirectory(path.dirname(outputPath));
940
1125
  const source = await fs.readFile(sourcePath, "utf8");
941
- const transformed = await transformLitsx(source, {
942
- filename: sourcePath,
1126
+ const transformed = await transformModuleSource(source, {
1127
+ projectRoot,
1128
+ sourcePath,
943
1129
  sourceMaps,
944
1130
  ssr,
945
1131
  });
@@ -1163,7 +1349,11 @@ export async function collectClientGraphInventory(entryPaths, options = {}) {
1163
1349
  if (visited.has(sourcePath)) return;
1164
1350
  visited.add(sourcePath);
1165
1351
  const source = await fs.readFile(sourcePath, "utf8");
1166
- const transformed = await transformLitsx(source, { filename: sourcePath, sourceMaps: false });
1352
+ const transformed = await transformModuleSource(source, {
1353
+ projectRoot,
1354
+ sourcePath,
1355
+ sourceMaps: false,
1356
+ });
1167
1357
  const isServer = isCompiledServerComponentModule(transformed.code);
1168
1358
  const isComponent = isCompiledClientBoundaryModule(transformed.code);
1169
1359
  if (isComponent && !isServer) {
package/src/constants.js CHANGED
@@ -13,8 +13,6 @@ export const DEPLOY_ASSETS_MANIFEST_FILENAME = "deploy-assets.json";
13
13
  export const DEPLOY_SERVER_MANIFEST_FILENAME = "deploy-server.json";
14
14
 
15
15
  export const MODULE_EXTENSIONS = [
16
- ".litsx",
17
- ".litsx.jsx",
18
16
  ".tsx",
19
17
  ".ts",
20
18
  ".jsx",
@@ -423,7 +423,7 @@ export async function createRequestRenderer({
423
423
 
424
424
  const importerPath = routeResult?.boundaryModule
425
425
  ?? routeResult?.route?.page
426
- ?? path.join(projectRoot, "app", "page.litsx");
426
+ ?? path.join(projectRoot, "app", "page.jsx");
427
427
  if (moduleId.startsWith("/")) {
428
428
  const projectSpecifier = `.${moduleId}`;
429
429
  return resolveProjectModuleSpecifier(projectRoot, importerPath, projectSpecifier);
package/src/index.js CHANGED
@@ -92,7 +92,7 @@ export { cookies } from "./request-context.js";
92
92
  export { headers } from "./request-context.js";
93
93
 
94
94
  /**
95
- * Signals a 404 response and renders the nearest `not-found.litsx` boundary when available.
95
+ * Signals a 404 response and renders the nearest `not-found.jsx` boundary when available.
96
96
  *
97
97
  * @returns {never}
98
98
  */
package/src/render.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
- import { html } from "lit";
4
3
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
- import { renderToString } from "@litsx/ssr";
4
+ import { html, renderToString } from "@litsx/ssr";
6
5
  import { discoverAppRouteHandlers, discoverAppRoutes, matchRoute } from "./app-discovery.js";
7
6
  import { importCompiledModule } from "./compiler.js";
8
7
  import { APP_DIRECTORY, MODULE_EXTENSIONS } from "./constants.js";
@@ -201,7 +201,7 @@ export function permanentRedirect(location) {
201
201
  }
202
202
 
203
203
  /**
204
- * Stops route execution with a 404 signal for the nearest `not-found.litsx` boundary.
204
+ * Stops route execution with a 404 signal for the nearest `not-found.jsx` boundary.
205
205
  *
206
206
  * @returns {never}
207
207
  */
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
- import { html } from "lit";
3
+ import { html } from "@litsx/ssr";
4
4
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
5
 
6
6
  const SEGMENT_MARKER_PREFIX = "evolit:segment";
package/src/scaffold.js CHANGED
@@ -16,6 +16,9 @@ async function writeSitePackageJson(targetDirectory, siteName) {
16
16
  version: "0.1.0",
17
17
  private: true,
18
18
  type: "module",
19
+ engines: {
20
+ node: "^22.18.0 || >=24.11.0",
21
+ },
19
22
  scripts: {
20
23
  dev: "evolit dev",
21
24
  build: "evolit build",
@@ -23,7 +26,7 @@ async function writeSitePackageJson(targetDirectory, siteName) {
23
26
  typecheck: "litsx-tsc -p jsconfig.json --noEmit",
24
27
  },
25
28
  dependencies: {
26
- "@litsx/core": "0.17.0-canary-feat-ssr-20260726130435",
29
+ "@litsx/core": "1.0.0-next.5",
27
30
  evolit: frameworkVersion,
28
31
  },
29
32
  devDependencies: {
package/src/server-api.js CHANGED
@@ -13,7 +13,7 @@ export { cookies } from "./request-context.js";
13
13
  export { headers } from "./request-context.js";
14
14
 
15
15
  /**
16
- * Signals a 404 response and renders the nearest `not-found.litsx` boundary when available.
16
+ * Signals a 404 response and renders the nearest `not-found.jsx` boundary when available.
17
17
  *
18
18
  * @returns {never}
19
19
  */
@@ -0,0 +1,37 @@
1
+ import { css } from "lit";
2
+
3
+ export default function FeatureCard({ title, body }) {
4
+ return (
5
+ <article class="card">
6
+ <h2 class="title">{title}</h2>
7
+ <p class="body">
8
+ {body}
9
+ </p>
10
+ </article>
11
+ );
12
+ }
13
+
14
+ FeatureCard.styles = css`
15
+ :host {
16
+ display: block;
17
+ }
18
+
19
+ .card {
20
+ padding: 24px;
21
+ border-radius: 18px;
22
+ background: rgba(255, 255, 255, 0.06);
23
+ border: 1px solid rgba(255, 255, 255, 0.08);
24
+ backdrop-filter: blur(12px);
25
+ }
26
+
27
+ .title {
28
+ margin: 0 0 12px;
29
+ font-size: 1.25rem;
30
+ }
31
+
32
+ .body {
33
+ margin: 0;
34
+ line-height: 1.7;
35
+ color: rgba(248, 247, 241, 0.78);
36
+ }
37
+ `;
@@ -1,4 +1,4 @@
1
- import FeatureCard from "./components/feature-card.litsx";
1
+ import FeatureCard from "@/app/components/feature-card";
2
2
 
3
3
  export default async function HomePage() {
4
4
  return (
@@ -20,7 +20,7 @@ export default async function HomePage() {
20
20
  <div class="features">
21
21
  <FeatureCard
22
22
  title="File Routing"
23
- body="Every app/page.litsx becomes an addressable route, with dynamic segments reserved for the next iteration."
23
+ body="Every app/page.jsx becomes an addressable route, with dynamic segments reserved for the next iteration."
24
24
  />
25
25
  <FeatureCard
26
26
  title="SSR Boundary"
@@ -28,7 +28,7 @@ export default async function HomePage() {
28
28
  />
29
29
  <FeatureCard
30
30
  title="Compiler Reuse"
31
- body="Authored .litsx source is compiled through the public @litsx/compiler facade instead of a parallel transform path."
31
+ body="Authored .jsx source is compiled through the public @litsx/compiler facade instead of a parallel transform path."
32
32
  />
33
33
  </div>
34
34
  </section>
@@ -6,9 +6,14 @@
6
6
  "allowJs": true,
7
7
  "checkJs": false,
8
8
  "strict": false,
9
- "jsx": "preserve",
9
+ "jsx": "react-jsx",
10
10
  "jsxImportSource": "@litsx/core",
11
11
  "allowArbitraryExtensions": true,
12
+ "paths": {
13
+ "@/*": [
14
+ "./*"
15
+ ]
16
+ },
12
17
  "plugins": [
13
18
  {
14
19
  "name": "@litsx/typescript"
@@ -1,35 +0,0 @@
1
- export default function FeatureCard({ title, body }) {
2
- static styles = `
3
- :host {
4
- display: block;
5
- }
6
-
7
- .card {
8
- padding: 24px;
9
- border-radius: 18px;
10
- background: rgba(255, 255, 255, 0.06);
11
- border: 1px solid rgba(255, 255, 255, 0.08);
12
- backdrop-filter: blur(12px);
13
- }
14
-
15
- .title {
16
- margin: 0 0 12px;
17
- font-size: 1.25rem;
18
- }
19
-
20
- .body {
21
- margin: 0;
22
- line-height: 1.7;
23
- color: rgba(248, 247, 241, 0.78);
24
- }
25
- `;
26
-
27
- return (
28
- <article class="card">
29
- <h2 class="title">{title}</h2>
30
- <p class="body">
31
- {body}
32
- </p>
33
- </article>
34
- );
35
- }