@firsthandjs/compiler 0.3.0 → 0.4.0

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  The build-time half of Firsthand. Nothing here ships to the browser.
4
4
 
5
- **Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/14-building.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/compiler.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
5
+ **Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/16-building.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/compiler.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
6
6
 
7
7
  ```ts
8
8
  // vite.config.ts
package/dist/api.d.ts CHANGED
@@ -7,7 +7,49 @@ export interface TransformOptions extends FirsthandPluginOptions {
7
7
  * source-map hops.
8
8
  */
9
9
  typescript?: boolean;
10
+ /** Produce a source map, so a debugger shows the JSX rather than the output. */
11
+ sourceMaps?: boolean;
10
12
  }
11
- /** Compiles one module. Used by the Vite plugin and by the compiler tests. */
13
+ /**
14
+ * A source map, in the shape every bundler expects one.
15
+ *
16
+ * Spelled out rather than typed as `object`, which is what it was: a bundler
17
+ * plugin's `transform` has to return something assignable to Rollup's
18
+ * `SourceMapInput`, and `object` is not. The mistake type-checks everywhere
19
+ * inside this repository and fails in the first application that puts the
20
+ * plugin into `vite.config.ts` — which is the one place it matters.
21
+ */
22
+ export interface SourceMap {
23
+ version: number;
24
+ mappings: string;
25
+ names: string[];
26
+ sources: string[];
27
+ sourcesContent?: string[];
28
+ sourceRoot?: string;
29
+ file?: string;
30
+ }
31
+ /** A compiled module, and the map back to what was written. */
32
+ export interface Compiled {
33
+ code: string;
34
+ /**
35
+ * A source map, when one was asked for.
36
+ *
37
+ * Without it a debugger shows the compiled output: hoisted templates and
38
+ * calls into the runtime protocol, rather than the JSX someone wrote. That
39
+ * is the difference between a framework you can step through and one you
40
+ * cannot, so the Vite plugin always asks.
41
+ */
42
+ map: SourceMap | null;
43
+ }
44
+ /**
45
+ * Compiles one module, and maps it back.
46
+ *
47
+ * The map is Babel's, which tracks the nodes this transform moves. It is
48
+ * approximate where a template was hoisted out of the markup it came from —
49
+ * there is no original position for a `<template>` that the compiler invented
50
+ * — and exact everywhere a statement survived.
51
+ */
52
+ export declare function compileModule(code: string, options?: TransformOptions): Compiled;
53
+ /** Compiles one module. Used by the compiler's own tests. */
12
54
  export declare function transform(code: string, options?: TransformOptions): string;
13
55
  //# sourceMappingURL=api.d.ts.map
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAwB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAW9E"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAwB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAKlB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,QAAQ,CAqBpF;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAE9E"}
@@ -129,6 +129,11 @@ function firsthandPlugin(_api, options = {}) {
129
129
  },
130
130
  CallExpression(path, state) {
131
131
  annotateComponent(path, state, options);
132
+ },
133
+ VariableDeclarator(path, state) {
134
+ if (options.devtools === true) {
135
+ nameCell(path, state);
136
+ }
132
137
  }
133
138
  }
134
139
  };
@@ -172,7 +177,7 @@ function annotateComponent(path, state, options) {
172
177
  }
173
178
  const name = declaredName(path);
174
179
  rewritePropsDestructuring(path, name, state);
175
- if (options.strictReactivity === true) {
180
+ if (options.strictReactivity !== false) {
176
181
  checkKeptReads(path.get("arguments.0"), name);
177
182
  }
178
183
  path.node.arguments = [
@@ -240,6 +245,36 @@ function readsOnly(node, propsName) {
240
245
  };
241
246
  return walk(node) && read;
242
247
  }
248
+ var NAMED = /* @__PURE__ */ new Map([
249
+ ["signal", "signal"],
250
+ ["computed", "computed"],
251
+ ["deepSignal", "signal"]
252
+ ]);
253
+ function nameCell(path, state) {
254
+ const init = path.node.init;
255
+ const target = path.node.id;
256
+ if (!t.isCallExpression(init) || !t.isIdentifier(target) || !t.isIdentifier(init.callee)) {
257
+ return;
258
+ }
259
+ const kind = NAMED.get(init.callee.name);
260
+ if (kind === void 0) {
261
+ return;
262
+ }
263
+ const binding = path.scope.getBinding(init.callee.name);
264
+ if (binding === void 0 || !isFirsthandImport(binding)) {
265
+ return;
266
+ }
267
+ const line = init.loc.start.line;
268
+ const source = state.filename;
269
+ const cut = Math.max(source.lastIndexOf("/"), source.lastIndexOf(String.fromCharCode(92)));
270
+ const where = `${source.slice(cut + 1)}:${String(line)}`;
271
+ const call = t.callExpression(runtime(state, "label"), [
272
+ init,
273
+ t.stringLiteral(kind),
274
+ t.stringLiteral(`${target.name} (${where})`)
275
+ ]);
276
+ path.node.init = call;
277
+ }
243
278
  function isFirsthandImport(binding) {
244
279
  const parent = binding.path.parentPath;
245
280
  if (!parent.isImportDeclaration()) {
@@ -286,7 +321,7 @@ function rewritePropsDestructuring(call, name, state) {
286
321
  for (const entry of references) {
287
322
  for (const reference of entry.paths) {
288
323
  if (entry.access !== propsId) {
289
- reference.replaceWith(t.cloneNode(entry.access));
324
+ reference.replaceWith(located(t.cloneNode(entry.access), reference.node));
290
325
  }
291
326
  }
292
327
  }
@@ -559,7 +594,10 @@ function emitAttribute(attribute, build, self, state, deferred, tag) {
559
594
  build.statements.push(
560
595
  expressionStatement2(
561
596
  t.callExpression(runtime(state, "bind"), [
562
- t.arrowFunctionExpression([], dynamicAttributeCall(name, value, self, state, tag))
597
+ located(
598
+ t.arrowFunctionExpression([], dynamicAttributeCall(name, value, self, state, tag)),
599
+ value
600
+ )
563
601
  ])
564
602
  )
565
603
  );
@@ -743,7 +781,7 @@ function emitChildren(children, build, self, state) {
743
781
  t.cloneNode(self),
744
782
  // A list part is already a thunk that owns its rows; wrapping it would
745
783
  // rebuild the whole list on every evaluation.
746
- entry.kind === "list" ? entry.expression : t.arrowFunctionExpression([], entry.expression)
784
+ entry.kind === "list" ? entry.expression : thunk(entry.expression)
747
785
  ];
748
786
  if (!isLast) {
749
787
  build.html.push("<!>");
@@ -867,23 +905,50 @@ function compileChildren(children, state) {
867
905
  }
868
906
  return result;
869
907
  }
908
+ function thunk(expression) {
909
+ const arrow = t.arrowFunctionExpression([], expression);
910
+ const loc = expression.loc;
911
+ if (loc !== null && loc !== void 0) {
912
+ arrow.loc = { ...loc, end: loc.start };
913
+ expression.loc = null;
914
+ }
915
+ return arrow;
916
+ }
870
917
  function expressionStatement2(expression) {
871
- return t.expressionStatement(expression);
918
+ return located(t.expressionStatement(expression), expression);
919
+ }
920
+ function located(node, source) {
921
+ if (source == null || source.loc == null) {
922
+ return node;
923
+ }
924
+ node.loc = source.loc;
925
+ return node;
872
926
  }
873
927
 
874
928
  // packages/compiler/src/api.ts
875
929
  import { transformSync } from "@babel/core";
876
930
  import jsxSyntax from "@babel/plugin-syntax-jsx";
877
931
  import typescriptSyntax from "@babel/plugin-syntax-typescript";
878
- function transform(code, options = {}) {
932
+ function compileModule(code, options = {}) {
879
933
  const result = transformSync(code, {
880
- ...options.filename === void 0 ? {} : { filename: options.filename },
934
+ ...options.filename === void 0 ? {} : {
935
+ filename: options.filename,
936
+ // The full path, not the basename Babel would default to. A map
937
+ // whose `sources` is `comp.tsx` resolves, relative to a module
938
+ // served at `/src/comp.tsx`, to that very URL — so a debugger ends
939
+ // up with two files under one address, shows both, and puts the
940
+ // breakpoint in the wrong one.
941
+ sourceFileName: options.filename
942
+ },
881
943
  babelrc: false,
882
944
  configFile: false,
883
- sourceMaps: false,
945
+ sourceMaps: options.sourceMaps === true,
884
946
  plugins: buildPlugins(options)
885
947
  });
886
- return result.code;
948
+ return { code: result.code, map: result.map };
949
+ }
950
+ function transform(code, options = {}) {
951
+ return compileModule(code, options).code;
887
952
  }
888
953
  function buildPlugins(options) {
889
954
  const plugins = [];
@@ -893,7 +958,11 @@ function buildPlugins(options) {
893
958
  plugins.push(jsxSyntax);
894
959
  plugins.push([
895
960
  firsthandPlugin,
896
- { packageName: options.packageName, strictReactivity: options.strictReactivity }
961
+ {
962
+ packageName: options.packageName,
963
+ strictReactivity: options.strictReactivity,
964
+ devtools: options.devtools
965
+ }
897
966
  ]);
898
967
  return plugins;
899
968
  }
@@ -901,5 +970,6 @@ function buildPlugins(options) {
901
970
  export {
902
971
  stableId,
903
972
  firsthandPlugin,
973
+ compileModule,
904
974
  transform
905
975
  };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export { default as firsthandPlugin } from './transform.js';
9
9
  export type { FirsthandPluginOptions } from './transform.js';
10
- export { transform } from './api.js';
10
+ export { compileModule, transform } from './api.js';
11
+ export type { Compiled, SourceMap, TransformOptions } from './api.js';
11
12
  export { stableId } from './ids.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACpD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import {
2
+ compileModule,
2
3
  firsthandPlugin,
3
4
  stableId,
4
5
  transform
5
- } from "./chunk-WW26NJL4.js";
6
+ } from "./chunk-UYA7HB5G.js";
6
7
  export {
8
+ compileModule,
7
9
  firsthandPlugin,
8
10
  stableId,
9
11
  transform
@@ -30,11 +30,25 @@ export interface FirsthandPluginOptions {
30
30
  /**
31
31
  * Refuse to compile a value that is read once in a setup and then kept.
32
32
  *
33
- * Off by default: reading once is legal and often deliberate. On, it is a
34
- * build error rather than a warning, because the cases it can see are the
35
- * unambiguous ones see `checkKeptReads` (ADR-0019).
33
+ * **On by default.** The rule only sees declarations whose initialiser is
34
+ * nothing but a read, which is the shape that is almost always a mistake;
35
+ * anything containing a call is left alone. `false` turns it off for a
36
+ * codebase that has such a read on purpose and would rather not mark it with
37
+ * `snapshot()` — see `checkKeptReads` (ADR-0019).
36
38
  */
37
39
  strictReactivity?: boolean;
40
+ /**
41
+ * Name the cells a module creates, for devtools.
42
+ *
43
+ * A runtime cannot see that `const count = signal(0)` is called `count`, and
44
+ * `new Error().stack` reports a position in the *compiled* module — the
45
+ * browser does not apply source maps to `error.stack`, so the line it names
46
+ * is not the line that was written. The compiler knows both, so it says so.
47
+ *
48
+ * Off by default and turned on by the Vite plugin while serving: a
49
+ * production build emits nothing.
50
+ */
51
+ devtools?: boolean;
38
52
  }
39
53
  export default function firsthandPlugin(_api: unknown, options?: FirsthandPluginOptions): PluginObject;
40
54
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAelC,UAAU,cAAc;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AAUD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAgDd"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAelC,UAAU,cAAc;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AAUD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAsDd"}
package/dist/vite.d.ts CHANGED
@@ -1,11 +1,15 @@
1
+ import { type SourceMap } from './api.js';
1
2
  import type { FirsthandPluginOptions } from './transform.js';
2
3
  /** Minimal shape of the Vite plugin contract, so the package needs no Vite dependency. */
3
4
  export interface VitePluginLike {
4
5
  name: string;
5
6
  enforce: 'pre';
7
+ configResolved(config: {
8
+ command: string;
9
+ }): void;
6
10
  transform(code: string, id: string): {
7
11
  code: string;
8
- map: null;
12
+ map: SourceMap | null;
9
13
  } | null;
10
14
  }
11
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CAmB9E"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,cAAc,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACrF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CA0B9E"}
package/dist/vite.js CHANGED
@@ -1,25 +1,28 @@
1
1
  import {
2
- transform
3
- } from "./chunk-WW26NJL4.js";
2
+ compileModule
3
+ } from "./chunk-UYA7HB5G.js";
4
4
 
5
5
  // packages/compiler/src/vite.ts
6
6
  function firsthand(options = {}) {
7
+ let serving = false;
7
8
  return {
8
9
  name: "firsthand",
9
10
  enforce: "pre",
11
+ configResolved(config) {
12
+ serving = config.command === "serve";
13
+ },
10
14
  transform(code, id) {
11
15
  const file = id.split("?")[0];
12
16
  if (!file.endsWith(".tsx") && !file.endsWith(".jsx")) {
13
17
  return null;
14
18
  }
15
- return {
16
- code: transform(code, {
17
- filename: file,
18
- typescript: file.endsWith(".tsx"),
19
- ...options
20
- }),
21
- map: null
22
- };
19
+ return compileModule(code, {
20
+ filename: file,
21
+ typescript: file.endsWith(".tsx"),
22
+ devtools: serving,
23
+ sourceMaps: true,
24
+ ...options
25
+ });
23
26
  }
24
27
  };
25
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firsthandjs/compiler",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Build-time TSX transform for Firsthand: static markup into templates, dynamic expressions into DOM parts.",
5
5
  "license": "MIT",
6
6
  "type": "module",