@statorjs/stator 1.2.1 → 1.2.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/package.json +2 -2
- package/src/compiler/virtual-code.ts +64 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statorjs/stator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Server-canonical web framework: business logic in composable state machines, UI as a thin renderer binding machine outputs to DOM positions. Ships TypeScript source (Vite/tsx-native by design).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"pino-pretty": "^13.1.3",
|
|
89
89
|
"vite": "^6.0.0",
|
|
90
90
|
"vitest": "^2.1.0",
|
|
91
|
-
"@statorjs/stator": "1.2.
|
|
91
|
+
"@statorjs/stator": "1.2.2"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --noEmit",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* load-bearing part.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import ts from 'typescript'
|
|
21
22
|
import { analyzeScriptClasses } from './client-script.ts'
|
|
22
23
|
import { componentPropsType, extractFrontmatterTypes } from './dts.ts'
|
|
23
24
|
import { type ScannedRegions, scanRegions } from './split.ts'
|
|
@@ -131,24 +132,29 @@ export function toVirtualCode(source: string): VirtualCodeResult {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/** Server component:
|
|
135
|
-
*
|
|
135
|
+
/** Server component: import/type/interface declarations hoist to module scope;
|
|
136
|
+
* the executable frontmatter body + the template (a JSX fragment) live inside
|
|
137
|
+
* the render function, so template expressions see the frontmatter's bindings.
|
|
138
|
+
*
|
|
139
|
+
* The body is deliberately NOT at module scope: it runs as a synchronous
|
|
140
|
+
* function body at runtime (compile.ts wraps it the same way), so modelling it
|
|
141
|
+
* as one here is what makes top-level `await` / `return` the TS errors they
|
|
142
|
+
* should be. Emitting the body at module scope (the previous shape) silently
|
|
143
|
+
* made them legal in-editor while diverging from runtime semantics. */
|
|
136
144
|
function buildServerTsx(regions: ScannedRegions): VirtualFile {
|
|
137
145
|
const mappings: VirtualMapping[] = []
|
|
138
|
-
const
|
|
146
|
+
const fm = regions.frontmatter?.content ?? ''
|
|
147
|
+
const fmOffset = regions.frontmatter?.contentOffset ?? 0
|
|
148
|
+
const userCode = fm + regions.template.content
|
|
139
149
|
let code =
|
|
140
150
|
injectImports(TEMPLATE_GLOBALS, '@statorjs/stator/template', userCode) +
|
|
141
151
|
AMBIENT_TYPE_IMPORTS +
|
|
142
152
|
STATOR_AMBIENT
|
|
143
153
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
code.length,
|
|
149
|
-
regions.frontmatter.content.length,
|
|
150
|
-
)
|
|
151
|
-
code += `${regions.frontmatter.content}\n`
|
|
154
|
+
const { hoisted, body } = splitFrontmatter(fm, fmOffset)
|
|
155
|
+
for (const seg of hoisted) {
|
|
156
|
+
push(mappings, seg.sourceOffset, code.length, seg.text.length)
|
|
157
|
+
code += `${seg.text}\n`
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
// A leading <!doctype> is static HTML, not JSX — drop it from the shell (no
|
|
@@ -162,17 +168,18 @@ function buildServerTsx(regions: ScannedRegions): VirtualFile {
|
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
// `export default function` gives importers a default export (`import X from
|
|
165
|
-
// './x.stator'`) and
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
// './x.stator'`) and a scope that closes over the frontmatter body. The param
|
|
172
|
+
// is typed from `Stator.props<P>()` (same extraction as the .d.ts generator),
|
|
173
|
+
// so `<Component bad={...}/>` in OTHER .stator files is checked in-editor.
|
|
174
|
+
// Named prop types resolve because their type/interface decls hoisted above.
|
|
175
|
+
const propsT = componentPropsType(extractFrontmatterTypes(fm).propsType, regions.template.content)
|
|
176
|
+
code += `export default function (_props: ${propsT}) {\n`
|
|
177
|
+
for (const seg of body) {
|
|
178
|
+
code += ' '
|
|
179
|
+
push(mappings, seg.sourceOffset, code.length, seg.text.length)
|
|
180
|
+
code += `${seg.text}\n`
|
|
181
|
+
}
|
|
182
|
+
code += ' return (<>'
|
|
176
183
|
push(mappings, tplOffset, code.length, tpl.length)
|
|
177
184
|
code += tpl
|
|
178
185
|
code += '</>);\n}\n'
|
|
@@ -180,6 +187,41 @@ function buildServerTsx(regions: ScannedRegions): VirtualFile {
|
|
|
180
187
|
return { lang: 'tsx', code, mappings }
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
interface FmSegment {
|
|
191
|
+
sourceOffset: number
|
|
192
|
+
text: string
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Split frontmatter into hoisted declarations (import/type/interface — must be
|
|
197
|
+
* module scope) and body statements (everything else — the render function).
|
|
198
|
+
* Mirrors the runtime classification in `compile.ts` `processFrontmatter`, so
|
|
199
|
+
* the editor models the same scoping. Each segment keeps its exact source range
|
|
200
|
+
* (`getStart`..`getText`) so the mapping stays a verbatim 1:1 run.
|
|
201
|
+
*/
|
|
202
|
+
function splitFrontmatter(
|
|
203
|
+
fm: string,
|
|
204
|
+
fmOffset: number,
|
|
205
|
+
): { hoisted: FmSegment[]; body: FmSegment[] } {
|
|
206
|
+
const hoisted: FmSegment[] = []
|
|
207
|
+
const body: FmSegment[] = []
|
|
208
|
+
if (!fm.trim()) return { hoisted, body }
|
|
209
|
+
const sf = ts.createSourceFile('fm.ts', fm, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
210
|
+
for (const stmt of sf.statements) {
|
|
211
|
+
const seg: FmSegment = { sourceOffset: fmOffset + stmt.getStart(sf), text: stmt.getText(sf) }
|
|
212
|
+
if (
|
|
213
|
+
ts.isImportDeclaration(stmt) ||
|
|
214
|
+
ts.isTypeAliasDeclaration(stmt) ||
|
|
215
|
+
ts.isInterfaceDeclaration(stmt)
|
|
216
|
+
) {
|
|
217
|
+
hoisted.push(seg)
|
|
218
|
+
} else {
|
|
219
|
+
body.push(seg)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { hoisted, body }
|
|
223
|
+
}
|
|
224
|
+
|
|
183
225
|
/** Client component: the `<script>` is the module. Emit it as TS so the class,
|
|
184
226
|
* `use()`/`machine()`, and imports get full intelligence. (Template-member
|
|
185
227
|
* resolution — `bind:text={theme.label}` → the class field — is a later phase.) */
|