@stacksjs/stx 0.2.170 → 0.2.172

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.
Files changed (44) hide show
  1. package/dist/cli.js +1263 -1168
  2. package/dist/client-script-bundler.js +2 -1
  3. package/dist/client-script.d.ts +1 -12
  4. package/dist/client-script.js +2 -149
  5. package/dist/codemod.d.ts +1 -24
  6. package/dist/codemod.js +61 -9
  7. package/dist/compression.d.ts +11 -4
  8. package/dist/compression.js +52 -13
  9. package/dist/craft.js +489 -415
  10. package/dist/expressions.d.ts +0 -21
  11. package/dist/expressions.js +12 -1
  12. package/dist/find-body-tag.js +29 -17
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.js +2 -0
  15. package/dist/page-action.d.ts +83 -0
  16. package/dist/page-action.js +64 -0
  17. package/dist/production-builder.js +1 -0
  18. package/dist/production-server.js +10 -3
  19. package/dist/pwa.js +398 -324
  20. package/dist/reactive-tracking.d.ts +81 -0
  21. package/dist/reactive-tracking.js +49 -0
  22. package/dist/reactivity.js +29 -23
  23. package/dist/render.js +5 -18
  24. package/dist/runtime-injection.js +25 -39
  25. package/dist/script-validation.d.ts +0 -7
  26. package/dist/script-validation.js +6 -4
  27. package/dist/signals-api.d.ts +18 -0
  28. package/dist/signals-api.js +26 -41
  29. package/dist/signals.js +93 -15
  30. package/dist/ssg.d.ts +8 -0
  31. package/dist/ssg.js +515 -441
  32. package/dist/state-management.js +2 -2
  33. package/dist/strip-literals.d.ts +40 -0
  34. package/dist/strip-literals.js +149 -0
  35. package/dist/stx-virtual-ts.d.ts +103 -1
  36. package/dist/stx-virtual-ts.js +124 -7
  37. package/dist/template-compiler.js +4 -2
  38. package/dist/template-hydrator.d.ts +1 -1
  39. package/dist/template-hydrator.js +13 -1
  40. package/dist/typecheck.d.ts +2 -14
  41. package/dist/typecheck.js +133 -11
  42. package/dist/types/config-types.d.ts +10 -3
  43. package/dist/variable-extractor.js +1 -1
  44. package/package.json +5 -5
package/dist/typecheck.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
4
4
  import { stateDir } from "./state-dir.js";
@@ -6,15 +6,20 @@ import {
6
6
  buildVirtualTypeScript,
7
7
  extractScriptBlocks,
8
8
  resolvePosition,
9
- serverContextDeclarations
9
+ clientPayloadDeclarations,
10
+ scrapedBridgeNames,
11
+ serverContextDeclarations,
12
+ substituteInterpolationsInPlace
10
13
  } from "./stx-virtual-ts.js";
11
14
  export {
12
15
  extractScriptBlocks,
13
16
  extractTemplateExpressions
14
17
  } from "./stx-virtual-ts.js";
15
- export function buildVirtualSource(block) {
16
- return `${(block.startLine > 1 ? `
17
- `.repeat(block.startLine - 1) : "") + block.code}
18
+ export function buildVirtualSource(block, serverCode = "") {
19
+ const leadingNewlines = block.startLine > 1 ? `
20
+ `.repeat(block.startLine - 1) : "", code = substituteInterpolationsInPlace(block.code), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode) : "";
21
+ return `${leadingNewlines + code}
22
+ ${payload}
18
23
  export {}
19
24
  `;
20
25
  }
@@ -34,8 +39,91 @@ export function findRuntimeTypeDeclarations() {
34
39
  return null;
35
40
  }
36
41
  const TSC_LINE_RE = /^(.*?)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.*)$/;
42
+ function readProjectPathAliases(startDir) {
43
+ let dir = path.resolve(startDir), configPath;
44
+ for (;; ) {
45
+ const candidate = path.join(dir, "tsconfig.json");
46
+ if (existsSync(candidate)) {
47
+ configPath = candidate;
48
+ break;
49
+ }
50
+ const parent = path.dirname(dir);
51
+ if (parent === dir)
52
+ break;
53
+ dir = parent;
54
+ }
55
+ if (!configPath)
56
+ return {};
57
+ const stripJsonc = (text) => {
58
+ let out = "", inString = !1, escaped = !1;
59
+ for (let i = 0;i < text.length; i++) {
60
+ const c = text[i];
61
+ if (inString) {
62
+ out += c;
63
+ if (escaped)
64
+ escaped = !1;
65
+ else if (c === "\\")
66
+ escaped = !0;
67
+ else if (c === '"')
68
+ inString = !1;
69
+ continue;
70
+ }
71
+ if (c === '"') {
72
+ inString = !0;
73
+ out += c;
74
+ continue;
75
+ }
76
+ if (c === "/" && text[i + 1] === "/") {
77
+ while (i < text.length && text[i] !== `
78
+ `)
79
+ i++;
80
+ out += `
81
+ `;
82
+ continue;
83
+ }
84
+ if (c === "/" && text[i + 1] === "*") {
85
+ i += 2;
86
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
87
+ i++;
88
+ i++;
89
+ continue;
90
+ }
91
+ out += c;
92
+ }
93
+ return out.replace(/,(\s*[}\]])/g, "$1");
94
+ }, seen = new Set;
95
+ let current = configPath, paths, baseUrl, owner = configPath;
96
+ while (current && !seen.has(current)) {
97
+ seen.add(current);
98
+ let parsed;
99
+ try {
100
+ parsed = JSON.parse(stripJsonc(readFileSync(current, "utf8")));
101
+ } catch {
102
+ break;
103
+ }
104
+ const co = parsed?.compilerOptions ?? {};
105
+ if (!paths && co.paths && typeof co.paths === "object") {
106
+ paths = co.paths;
107
+ owner = current;
108
+ }
109
+ if (!baseUrl && typeof co.baseUrl === "string")
110
+ baseUrl = path.resolve(path.dirname(current), co.baseUrl);
111
+ if (paths && baseUrl)
112
+ break;
113
+ current = typeof parsed?.extends === "string" ? path.resolve(path.dirname(current), parsed.extends.endsWith(".json") ? parsed.extends : `${parsed.extends}.json`) : void 0;
114
+ }
115
+ if (!paths)
116
+ return {};
117
+ const base = baseUrl ?? path.dirname(owner), absolute = {};
118
+ for (const [pattern, targets] of Object.entries(paths)) {
119
+ if (!Array.isArray(targets))
120
+ continue;
121
+ absolute[pattern] = targets.map((t) => typeof t === "string" && !path.isAbsolute(t) ? path.resolve(base, t) : t);
122
+ }
123
+ return { paths: absolute };
124
+ }
37
125
  export async function typecheckStxFiles(files, options = {}) {
38
- const checkClient = options.client !== !1, checkServer = options.server !== !1, checkTemplates = options.templates !== !1, virtualFiles = new Map, checkedFiles = [];
126
+ const checkClient = options.client !== !1, checkServer = options.server !== !1, checkTemplates = options.templates !== !1, virtualFiles = new Map, scrapedByFile = new Map, checkedFiles = [];
39
127
  let blockCount = 0, expressionCount = 0;
40
128
  const runtimeTypes = findRuntimeTypeDeclarations();
41
129
  for (const file of files) {
@@ -45,7 +133,14 @@ export async function typecheckStxFiles(files, options = {}) {
45
133
  if (b.kind === "client")
46
134
  return checkClient;
47
135
  return !1;
48
- }), templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
136
+ }), serverCode = extractScriptBlocks(source).filter((block) => block.kind === "server").map((block) => block.code).join(`
137
+ `);
138
+ if (checkClient && serverCode) {
139
+ const clientBlocks = blocks.filter((block) => block.kind === "client"), names = [...new Set(clientBlocks.flatMap((block) => scrapedBridgeNames(serverCode, block.code)))];
140
+ if (names.length > 0)
141
+ scrapedByFile.set(file, { names, line: clientBlocks[0].startLine });
142
+ }
143
+ const templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
49
144
  if (blocks.length === 0 && expressions === 0)
50
145
  continue;
51
146
  checkedFiles.push(file);
@@ -53,7 +148,7 @@ export async function typecheckStxFiles(files, options = {}) {
53
148
  expressionCount += expressions;
54
149
  blocks.forEach((block, i) => {
55
150
  virtualFiles.set(virtualPathFor(file, block.kind, i), {
56
- source: buildVirtualSource(block),
151
+ source: buildVirtualSource(block, serverCode),
57
152
  origin: file,
58
153
  kind: block.kind
59
154
  });
@@ -68,7 +163,7 @@ export async function typecheckStxFiles(files, options = {}) {
68
163
  }
69
164
  if (virtualFiles.size === 0)
70
165
  return { diagnostics: [], checkedFiles, blockCount: 0, expressionCount: 0 };
71
- const ambient = [...options.extraLibs ?? []];
166
+ const ambient = (options.extraLibs ?? []).map((lib) => path.resolve(lib));
72
167
  if (runtimeTypes)
73
168
  ambient.push(runtimeTypes);
74
169
  const globalsDts = `${stateDir()}/typecheck/__stx_globals.d.ts`, globalDecls = [
@@ -100,6 +195,7 @@ export async function typecheckStxFiles(files, options = {}) {
100
195
  resolveJsonModule: !0,
101
196
  lib: ["ESNext", "DOM", "DOM.Iterable"],
102
197
  types: [],
198
+ ...readProjectPathAliases(files.length > 0 ? path.dirname(path.resolve(files[0])) : process.cwd()),
103
199
  ...options.compilerOptions ?? {}
104
200
  },
105
201
  files: [...writtenToOrigin.keys()].map((name) => `${workDir}/${name}`).concat(globalsDts, ...ambient)
@@ -108,12 +204,18 @@ export async function typecheckStxFiles(files, options = {}) {
108
204
  const proc = Bun.spawnSync(["bun", "x", "tsc", "-p", tsconfigPath, "--pretty", "false"], {
109
205
  stdout: "pipe",
110
206
  stderr: "pipe"
111
- }), output = new TextDecoder().decode(proc.stdout) + new TextDecoder().decode(proc.stderr), diagnostics = [];
207
+ }), output = new TextDecoder().decode(proc.stdout) + new TextDecoder().decode(proc.stderr), diagnostics = [], exitCode = proc.exitCode ?? 0;
208
+ let printedDiagnostics = 0;
209
+ const ambientBasenames = new Set(ambient.map((lib) => lib.split("/").pop()));
210
+ let ambientDiagnostics = 0;
112
211
  for (const rawLine of output.split(`
113
212
  `)) {
114
213
  const m = rawLine.match(TSC_LINE_RE);
115
214
  if (!m)
116
215
  continue;
216
+ printedDiagnostics++;
217
+ if (ambientBasenames.has(m[1].split("/").pop()))
218
+ ambientDiagnostics++;
117
219
  const [, filePart, lineStr, colStr, category, code, message] = m, entry = writtenToOrigin.get(filePart.split("/").pop());
118
220
  if (!entry)
119
221
  continue;
@@ -122,6 +224,8 @@ export async function typecheckStxFiles(files, options = {}) {
122
224
  const at = resolvePosition(entry.virtual, line, column);
123
225
  if (!at?.expression)
124
226
  continue;
227
+ if (Number(code) === 2774)
228
+ continue;
125
229
  line = at.line;
126
230
  column = at.column;
127
231
  expression = at.expression.code.trim();
@@ -137,6 +241,20 @@ export async function typecheckStxFiles(files, options = {}) {
137
241
  expression
138
242
  });
139
243
  }
244
+ if (checkClient)
245
+ for (const [file, scraped] of scrapedByFile) {
246
+ if (scraped.names.length === 0)
247
+ continue;
248
+ diagnostics.push({
249
+ file,
250
+ line: scraped.line,
251
+ column: 1,
252
+ code: 0,
253
+ message: `${scraped.names.length === 1 ? "1 value reaches" : `${scraped.names.length} values reach`} this block on the implicit bridge and ${scraped.names.length === 1 ? "is" : "are"} untyped: ${scraped.names.join(", ")}. Declare them with defineClientPayload({ ${scraped.names.join(", ")} }) in the server block to have them checked.`,
254
+ category: "warning",
255
+ blockKind: "client"
256
+ });
257
+ }
140
258
  const seen = new Set, unique = diagnostics.filter((d) => {
141
259
  const key = `${d.file}:${d.line}:${d.column}:${d.code}:${d.message}`;
142
260
  if (seen.has(key))
@@ -145,7 +263,11 @@ export async function typecheckStxFiles(files, options = {}) {
145
263
  return !0;
146
264
  });
147
265
  unique.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
148
- return { diagnostics: unique, checkedFiles, blockCount, expressionCount };
266
+ const brokenLib = ambientDiagnostics > 0 && unique.length === 0, failure = brokenLib || exitCode !== 0 && printedDiagnostics === 0 ? `${brokenLib ? "a file passed with --lib has errors of its own, so nothing in your code was checked" : `tsc exited ${exitCode} without reporting any diagnostic, so nothing was checked`}. ` + "A syntax error in an ambient declaration file does this \u2014 skipLibCheck suppresses " + `SEMANTIC diagnostics only. tsc said:
267
+ ${output.trim().split(`
268
+ `).slice(0, 12).join(`
269
+ `) || "(no output)"}` : void 0;
270
+ return { diagnostics: unique, checkedFiles, blockCount, expressionCount, failure };
149
271
  }
150
272
  export function formatTypecheckDiagnostics(diagnostics) {
151
273
  return diagnostics.map((d) => {
@@ -344,6 +344,16 @@ export interface StrictModeConfig {
344
344
  failOnViolation?: boolean;
345
345
  /** Pattern strings to allow through validation (matched against message or regex source) */
346
346
  allowPatterns?: string[];
347
+ /**
348
+ * What to do when a `<script client>` block does not bundle.
349
+ *
350
+ * `'warn'` (default) ships the ORIGINAL, unbundled source so the page still
351
+ * renders — right for a dev server, and why the error overlay exists.
352
+ * `'error'` refuses instead, for anyone who would rather nothing rendered
353
+ * than a page whose bindings quietly do nothing. `stx build` fails on a
354
+ * bundle error either way. See #1884.
355
+ */
356
+ bundlerFallback?: 'warn' | 'error';
347
357
  }
348
358
  /**
349
359
  * Real-time broadcasting configuration.
@@ -655,9 +665,6 @@ export interface StxConfig {
655
665
  * rendered than a page whose bindings quietly do nothing. `stx build` fails on
656
666
  * a bundle error either way. See #1884.
657
667
  */
658
- strict?: {
659
- bundlerFallback?: 'warn' | 'error';
660
- };
661
668
  /** Directory to store cached templates, defaults to `cache` inside `stateDir` */
662
669
  cachePath: string;
663
670
  /** Custom directives registered by the user */
@@ -676,7 +676,7 @@ export function convertToCommonJS(scriptContent, filePath) {
676
676
  };
677
677
  if (defaultImportMatch) {
678
678
  const [, name, source] = defaultImportMatch, resolved = resolveSource(source);
679
- convertedLines.push(`const ${name} = await import('${resolved}')`);
679
+ convertedLines.push(`const ${name} = (await import('${resolved}')).default`);
680
680
  convertedLines.push(`module.exports.${name} = ${name};`);
681
681
  } else if (namedImportMatch) {
682
682
  const [, names, source] = namedImportMatch, resolved = resolveSource(source), specifiers = [], exported = [], isStxRuntime = STX_RUNTIME_SPECIFIERS.has(source);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.170",
4
+ "version": "0.2.172",
5
5
  "description": "A performant UI Framework. Powered by Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -117,12 +117,12 @@
117
117
  "dependencies": {
118
118
  "@cwcss/crosswind": "^0.2.14",
119
119
  "@stacksjs/clapp": "^0.2.0",
120
- "@stacksjs/desktop": "0.2.170",
121
- "@stacksjs/sanitizer": "0.2.170",
120
+ "@stacksjs/desktop": "0.2.172",
121
+ "@stacksjs/sanitizer": "0.2.172",
122
122
  "@stacksjs/ts-i18n": "^0.1.11",
123
- "bun-plugin-stx": "0.2.170",
123
+ "bun-plugin-stx": "0.2.172",
124
124
  "bunfig": "^0.15.17",
125
- "stx-router": "0.2.170",
125
+ "stx-router": "0.2.172",
126
126
  "ts-broadcasting": "^0.0.9",
127
127
  "ts-images": "^0.1.9",
128
128
  "ts-syntax-highlighter": "^0.2.1"