@esportsplus/reactivity 0.23.0 → 0.23.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.
Files changed (44) hide show
  1. package/build/transformer/detector.js +38 -0
  2. package/build/transformer/index.d.ts +9 -0
  3. package/build/transformer/{core/index.js → index.js} +9 -18
  4. package/build/transformer/plugins/esbuild.d.ts +1 -3
  5. package/build/transformer/plugins/esbuild.js +5 -4
  6. package/build/transformer/plugins/tsc.js +1 -1
  7. package/build/transformer/plugins/vite.d.ts +1 -3
  8. package/build/transformer/plugins/vite.js +4 -4
  9. package/build/transformer/{core/transforms/reactive-array.d.ts → transforms/array.d.ts} +1 -1
  10. package/build/transformer/transforms/array.js +93 -0
  11. package/build/transformer/{core/transforms/reactive-object.d.ts → transforms/object.d.ts} +1 -1
  12. package/build/transformer/transforms/object.js +163 -0
  13. package/build/transformer/{core/transforms/reactive-primitives.d.ts → transforms/primitives.d.ts} +1 -1
  14. package/build/transformer/transforms/primitives.js +335 -0
  15. package/build/transformer/{core/transforms → transforms}/utilities.d.ts +1 -2
  16. package/build/transformer/transforms/utilities.js +73 -0
  17. package/build/types.d.ts +1 -4
  18. package/package.json +8 -12
  19. package/readme.md +0 -24
  20. package/src/transformer/detector.ts +65 -0
  21. package/src/transformer/{core/index.ts → index.ts} +10 -25
  22. package/src/transformer/plugins/esbuild.ts +5 -6
  23. package/src/transformer/plugins/tsc.ts +1 -1
  24. package/src/transformer/plugins/vite.ts +4 -8
  25. package/src/transformer/transforms/array.ts +143 -0
  26. package/src/transformer/transforms/object.ts +251 -0
  27. package/src/transformer/transforms/primitives.ts +461 -0
  28. package/src/transformer/transforms/utilities.ts +119 -0
  29. package/src/types.ts +0 -5
  30. package/build/transformer/core/detector.js +0 -6
  31. package/build/transformer/core/index.d.ts +0 -10
  32. package/build/transformer/core/transforms/auto-dispose.d.ts +0 -3
  33. package/build/transformer/core/transforms/auto-dispose.js +0 -116
  34. package/build/transformer/core/transforms/reactive-array.js +0 -89
  35. package/build/transformer/core/transforms/reactive-object.js +0 -155
  36. package/build/transformer/core/transforms/reactive-primitives.js +0 -325
  37. package/build/transformer/core/transforms/utilities.js +0 -57
  38. package/src/transformer/core/detector.ts +0 -12
  39. package/src/transformer/core/transforms/auto-dispose.ts +0 -194
  40. package/src/transformer/core/transforms/reactive-array.ts +0 -140
  41. package/src/transformer/core/transforms/reactive-object.ts +0 -244
  42. package/src/transformer/core/transforms/reactive-primitives.ts +0 -459
  43. package/src/transformer/core/transforms/utilities.ts +0 -95
  44. /package/build/transformer/{core/detector.d.ts → detector.d.ts} +0 -0
@@ -0,0 +1,38 @@
1
+ import { mightNeedTransform as checkTransform } from '@esportsplus/typescript/transformer';
2
+ import ts from 'typescript';
3
+ const REACTIVE_REGEX = /\breactive\b/;
4
+ function visit(ctx, node) {
5
+ if (ctx.hasImport && ctx.hasUsage) {
6
+ return;
7
+ }
8
+ if (ts.isImportDeclaration(node) &&
9
+ node.importClause?.namedBindings &&
10
+ ts.isNamedImports(node.importClause.namedBindings)) {
11
+ let elements = node.importClause.namedBindings.elements;
12
+ for (let i = 0, n = elements.length; i < n; i++) {
13
+ let el = elements[i], name = el.propertyName?.text ?? el.name.text;
14
+ if (name === 'reactive') {
15
+ ctx.hasImport = true;
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ if (ts.isCallExpression(node) &&
21
+ ts.isIdentifier(node.expression) &&
22
+ node.expression.text === 'reactive') {
23
+ ctx.hasUsage = true;
24
+ }
25
+ ts.forEachChild(node, n => visit(ctx, n));
26
+ }
27
+ const mightNeedTransform = (code) => {
28
+ if (!checkTransform(code, { regex: REACTIVE_REGEX })) {
29
+ return false;
30
+ }
31
+ let ctx = {
32
+ hasImport: false,
33
+ hasUsage: false
34
+ };
35
+ visit(ctx, ts.createSourceFile('detect.ts', code, ts.ScriptTarget.Latest, false));
36
+ return ctx.hasImport && ctx.hasUsage;
37
+ };
38
+ export { mightNeedTransform };
@@ -0,0 +1,9 @@
1
+ import type { TransformResult } from '../types.js';
2
+ import { mightNeedTransform } from './detector.js';
3
+ import ts from 'typescript';
4
+ declare const createTransformer: () => ts.TransformerFactory<ts.SourceFile>;
5
+ declare const transform: (sourceFile: ts.SourceFile) => TransformResult;
6
+ export { createTransformer, mightNeedTransform, transform };
7
+ export { transformReactiveArrays } from './transforms/array.js';
8
+ export { transformReactiveObjects } from './transforms/object.js';
9
+ export { transformReactivePrimitives } from './transforms/primitives.js';
@@ -1,18 +1,17 @@
1
- import { injectAutoDispose } from './transforms/auto-dispose.js';
2
1
  import { mightNeedTransform } from './detector.js';
3
- import { transformReactiveArrays } from './transforms/reactive-array.js';
4
- import { transformReactiveObjects } from './transforms/reactive-object.js';
5
- import { transformReactivePrimitives } from './transforms/reactive-primitives.js';
2
+ import { transformReactiveArrays } from './transforms/array.js';
3
+ import { transformReactiveObjects } from './transforms/object.js';
4
+ import { transformReactivePrimitives } from './transforms/primitives.js';
6
5
  import ts from 'typescript';
7
- const createTransformer = (options) => {
6
+ const createTransformer = () => {
8
7
  return () => {
9
8
  return (sourceFile) => {
10
- let result = transform(sourceFile, options);
9
+ let result = transform(sourceFile);
11
10
  return result.transformed ? result.sourceFile : sourceFile;
12
11
  };
13
12
  };
14
13
  };
15
- const transform = (sourceFile, options) => {
14
+ const transform = (sourceFile) => {
16
15
  let bindings = new Map(), code = sourceFile.getFullText(), current = sourceFile, original = code, result;
17
16
  if (!mightNeedTransform(code)) {
18
17
  return { code, sourceFile, transformed: false };
@@ -32,13 +31,6 @@ const transform = (sourceFile, options) => {
32
31
  current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
33
32
  code = result;
34
33
  }
35
- if (options?.autoDispose) {
36
- result = injectAutoDispose(current);
37
- if (result !== code) {
38
- current = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
39
- code = result;
40
- }
41
- }
42
34
  if (code === original) {
43
35
  return { code, sourceFile, transformed: false };
44
36
  }
@@ -49,7 +41,6 @@ const transform = (sourceFile, options) => {
49
41
  };
50
42
  };
51
43
  export { createTransformer, mightNeedTransform, transform };
52
- export { injectAutoDispose } from './transforms/auto-dispose.js';
53
- export { transformReactiveArrays } from './transforms/reactive-array.js';
54
- export { transformReactiveObjects } from './transforms/reactive-object.js';
55
- export { transformReactivePrimitives } from './transforms/reactive-primitives.js';
44
+ export { transformReactiveArrays } from './transforms/array.js';
45
+ export { transformReactiveObjects } from './transforms/object.js';
46
+ export { transformReactivePrimitives } from './transforms/primitives.js';
@@ -1,5 +1,3 @@
1
1
  import type { Plugin } from 'esbuild';
2
- import type { TransformOptions } from '../../types.js';
3
- declare const _default: (options?: TransformOptions) => Plugin;
2
+ declare const _default: () => Plugin;
4
3
  export default _default;
5
- export type { TransformOptions as PluginOptions };
@@ -1,17 +1,18 @@
1
- import { mightNeedTransform, transform } from '../../transformer/core/index.js';
1
+ import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
2
+ import { mightNeedTransform, transform } from '../../transformer/index.js';
2
3
  import fs from 'fs';
3
4
  import ts from 'typescript';
4
- export default (options) => {
5
+ export default () => {
5
6
  return {
6
7
  name: '@esportsplus/reactivity/plugin-esbuild',
7
8
  setup(build) {
8
- build.onLoad({ filter: /\.[tj]sx?$/ }, async (args) => {
9
+ build.onLoad({ filter: TRANSFORM_PATTERN }, async (args) => {
9
10
  let code = await fs.promises.readFile(args.path, 'utf8');
10
11
  if (!mightNeedTransform(code)) {
11
12
  return null;
12
13
  }
13
14
  try {
14
- let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, options);
15
+ let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile);
15
16
  if (!result.transformed) {
16
17
  return null;
17
18
  }
@@ -1,4 +1,4 @@
1
- import { createTransformer } from '../../transformer/core/index.js';
1
+ import { createTransformer } from '../../transformer/index.js';
2
2
  export default (_program) => {
3
3
  return createTransformer();
4
4
  };
@@ -1,5 +1,3 @@
1
1
  import type { Plugin } from 'vite';
2
- import type { TransformOptions } from '../../types.js';
3
- declare const _default: (options?: TransformOptions) => Plugin;
2
+ declare const _default: () => Plugin;
4
3
  export default _default;
5
- export type { TransformOptions as PluginOptions };
@@ -1,7 +1,7 @@
1
- import { mightNeedTransform, transform } from '../../transformer/core/index.js';
1
+ import { TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
2
+ import { mightNeedTransform, transform } from '../../transformer/index.js';
2
3
  import ts from 'typescript';
3
- const TRANSFORM_PATTERN = /\.[tj]sx?$/;
4
- export default (options) => {
4
+ export default () => {
5
5
  return {
6
6
  enforce: 'pre',
7
7
  name: '@esportsplus/reactivity/plugin-vite',
@@ -13,7 +13,7 @@ export default (options) => {
13
13
  return null;
14
14
  }
15
15
  try {
16
- let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, options);
16
+ let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile);
17
17
  if (!result.transformed) {
18
18
  return null;
19
19
  }
@@ -1,4 +1,4 @@
1
- import type { Bindings } from '../../../types.js';
1
+ import type { Bindings } from '../../types.js';
2
2
  import ts from 'typescript';
3
3
  declare const transformReactiveArrays: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
4
  export { transformReactiveArrays };
@@ -0,0 +1,93 @@
1
+ import { applyReplacements } from './utilities.js';
2
+ import ts from 'typescript';
3
+ function getExpressionName(node) {
4
+ if (ts.isIdentifier(node)) {
5
+ return node.text;
6
+ }
7
+ if (ts.isPropertyAccessExpression(node)) {
8
+ return getPropertyPath(node);
9
+ }
10
+ return null;
11
+ }
12
+ function getPropertyPath(node) {
13
+ let current = node, parts = [];
14
+ while (ts.isPropertyAccessExpression(current)) {
15
+ parts.unshift(current.name.text);
16
+ current = current.expression;
17
+ }
18
+ if (ts.isIdentifier(current)) {
19
+ parts.unshift(current.text);
20
+ return parts.join('.');
21
+ }
22
+ return null;
23
+ }
24
+ function isAssignmentTarget(node) {
25
+ let parent = node.parent;
26
+ if ((ts.isBinaryExpression(parent) && parent.left === node) ||
27
+ ts.isPostfixUnaryExpression(parent) ||
28
+ ts.isPrefixUnaryExpression(parent)) {
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ function visit(ctx, node) {
34
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
35
+ if (ts.isIdentifier(node.initializer) && ctx.bindings.get(node.initializer.text) === 'array') {
36
+ ctx.bindings.set(node.name.text, 'array');
37
+ }
38
+ if (ts.isPropertyAccessExpression(node.initializer)) {
39
+ let path = getPropertyPath(node.initializer);
40
+ if (path && ctx.bindings.get(path) === 'array') {
41
+ ctx.bindings.set(node.name.text, 'array');
42
+ }
43
+ }
44
+ }
45
+ if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) && node.parameters) {
46
+ for (let i = 0, n = node.parameters.length; i < n; i++) {
47
+ let param = node.parameters[i];
48
+ if ((ts.isIdentifier(param.name) && param.type) &&
49
+ ts.isTypeReferenceNode(param.type) &&
50
+ ts.isIdentifier(param.type.typeName) &&
51
+ param.type.typeName.text === 'ReactiveArray') {
52
+ ctx.bindings.set(param.name.text, 'array');
53
+ }
54
+ }
55
+ }
56
+ if (ts.isPropertyAccessExpression(node) &&
57
+ node.name.text === 'length' &&
58
+ !isAssignmentTarget(node)) {
59
+ let name = getExpressionName(node.expression);
60
+ if (name && ctx.bindings.get(name) === 'array') {
61
+ let objText = node.expression.getText(ctx.sourceFile);
62
+ ctx.replacements.push({
63
+ end: node.end,
64
+ newText: `${objText}.$length()`,
65
+ start: node.pos
66
+ });
67
+ }
68
+ }
69
+ if (ts.isBinaryExpression(node) &&
70
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
71
+ ts.isElementAccessExpression(node.left)) {
72
+ let elemAccess = node.left, objName = getExpressionName(elemAccess.expression);
73
+ if (objName && ctx.bindings.get(objName) === 'array') {
74
+ let indexText = elemAccess.argumentExpression.getText(ctx.sourceFile), objText = elemAccess.expression.getText(ctx.sourceFile), valueText = node.right.getText(ctx.sourceFile);
75
+ ctx.replacements.push({
76
+ end: node.end,
77
+ newText: `${objText}.$set(${indexText}, ${valueText})`,
78
+ start: node.pos
79
+ });
80
+ }
81
+ }
82
+ ts.forEachChild(node, n => visit(ctx, n));
83
+ }
84
+ const transformReactiveArrays = (sourceFile, bindings) => {
85
+ let code = sourceFile.getFullText(), ctx = {
86
+ bindings,
87
+ replacements: [],
88
+ sourceFile
89
+ };
90
+ visit(ctx, sourceFile);
91
+ return applyReplacements(code, ctx.replacements);
92
+ };
93
+ export { transformReactiveArrays };
@@ -1,4 +1,4 @@
1
- import type { Bindings } from '../../../types.js';
1
+ import type { Bindings } from '../../types.js';
2
2
  import ts from 'typescript';
3
3
  declare const transformReactiveObjects: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
4
  export { transformReactiveObjects };
@@ -0,0 +1,163 @@
1
+ import { uid } from '@esportsplus/typescript/transformer';
2
+ import { addMissingImports, applyReplacements } from './utilities.js';
3
+ import ts from 'typescript';
4
+ const CLASS_NAME_REGEX = /class (\w+)/;
5
+ const EXTRA_IMPORTS = [
6
+ { module: '@esportsplus/reactivity/constants', specifier: 'REACTIVE_OBJECT' },
7
+ { module: '@esportsplus/reactivity/reactive/array', specifier: 'ReactiveArray' }
8
+ ];
9
+ function analyzeProperty(prop, sourceFile) {
10
+ if (!ts.isPropertyAssignment(prop)) {
11
+ return null;
12
+ }
13
+ let key;
14
+ if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
15
+ key = prop.name.text;
16
+ }
17
+ else {
18
+ return null;
19
+ }
20
+ let value = prop.initializer, valueText = value.getText(sourceFile);
21
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
22
+ return { key, type: 'computed', valueText };
23
+ }
24
+ if (ts.isArrayLiteralExpression(value)) {
25
+ return { key, type: 'array', valueText };
26
+ }
27
+ return { key, type: 'signal', valueText };
28
+ }
29
+ function buildClassCode(className, properties) {
30
+ let accessors = [], disposeStatements = [], fields = [];
31
+ fields.push(`[REACTIVE_OBJECT] = true;`);
32
+ for (let i = 0, n = properties.length; i < n; i++) {
33
+ let { key, type, valueText } = properties[i];
34
+ if (type === 'signal') {
35
+ let param = uid('v');
36
+ fields.push(`#${key} = signal(${valueText});`);
37
+ accessors.push(`get ${key}() { return read(this.#${key}); }`);
38
+ accessors.push(`set ${key}(${param}) { set(this.#${key}, ${param}); }`);
39
+ }
40
+ else if (type === 'array') {
41
+ let elements = valueText.slice(1, -1);
42
+ fields.push(`${key} = new ReactiveArray(${elements});`);
43
+ disposeStatements.push(`this.${key}.dispose();`);
44
+ }
45
+ else if (type === 'computed') {
46
+ fields.push(`#${key}: Computed<unknown> | null = null;`);
47
+ accessors.push(`get ${key}() { return read(this.#${key} ??= computed(${valueText})); }`);
48
+ disposeStatements.push(`if (this.#${key}) dispose(this.#${key});`);
49
+ }
50
+ }
51
+ return `
52
+ class ${className} {
53
+ ${fields.join('\n')}
54
+ ${accessors.join('\n')}
55
+
56
+ dispose() {
57
+ ${disposeStatements.length > 0 ? disposeStatements.join('\n') : ''}
58
+ }
59
+ }
60
+ `;
61
+ }
62
+ function visit(ctx, node) {
63
+ if (ts.isImportDeclaration(node)) {
64
+ ctx.lastImportEnd = node.end;
65
+ if (ts.isStringLiteral(node.moduleSpecifier) &&
66
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')) {
67
+ let clause = node.importClause;
68
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
69
+ let elements = clause.namedBindings.elements;
70
+ for (let i = 0, n = elements.length; i < n; i++) {
71
+ if (elements[i].name.text === 'reactive') {
72
+ ctx.hasReactiveImport = true;
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ if (ctx.hasReactiveImport &&
80
+ ts.isCallExpression(node) &&
81
+ ts.isIdentifier(node.expression) &&
82
+ node.expression.text === 'reactive') {
83
+ let arg = node.arguments[0];
84
+ if (arg && ts.isObjectLiteralExpression(arg)) {
85
+ let varName = null;
86
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
87
+ varName = node.parent.name.text;
88
+ ctx.bindings.set(varName, 'object');
89
+ }
90
+ let needsImports = new Set(), properties = [];
91
+ needsImports.add('REACTIVE_OBJECT');
92
+ let props = arg.properties;
93
+ for (let i = 0, n = props.length; i < n; i++) {
94
+ let prop = props[i];
95
+ if (ts.isSpreadAssignment(prop)) {
96
+ ts.forEachChild(node, n => visit(ctx, n));
97
+ return;
98
+ }
99
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
100
+ if (!analyzed) {
101
+ ts.forEachChild(node, n => visit(ctx, n));
102
+ return;
103
+ }
104
+ properties.push(analyzed);
105
+ if (analyzed.type === 'signal') {
106
+ needsImports.add('read');
107
+ needsImports.add('set');
108
+ needsImports.add('signal');
109
+ }
110
+ else if (analyzed.type === 'array') {
111
+ needsImports.add('ReactiveArray');
112
+ if (varName) {
113
+ ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
114
+ }
115
+ }
116
+ else if (analyzed.type === 'computed') {
117
+ needsImports.add('computed');
118
+ needsImports.add('dispose');
119
+ needsImports.add('read');
120
+ }
121
+ }
122
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
123
+ ctx.calls.push({
124
+ end: node.end,
125
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
126
+ needsImports,
127
+ start: node.pos,
128
+ varName
129
+ });
130
+ }
131
+ }
132
+ ts.forEachChild(node, n => visit(ctx, n));
133
+ }
134
+ const transformReactiveObjects = (sourceFile, bindings) => {
135
+ let code = sourceFile.getFullText(), ctx = {
136
+ allNeededImports: new Set(),
137
+ bindings,
138
+ calls: [],
139
+ hasReactiveImport: false,
140
+ lastImportEnd: 0,
141
+ sourceFile
142
+ };
143
+ visit(ctx, sourceFile);
144
+ if (ctx.calls.length === 0) {
145
+ return code;
146
+ }
147
+ let replacements = [];
148
+ replacements.push({
149
+ end: ctx.lastImportEnd,
150
+ newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
151
+ start: 0
152
+ });
153
+ for (let i = 0, n = ctx.calls.length; i < n; i++) {
154
+ let call = ctx.calls[i], classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
155
+ replacements.push({
156
+ end: call.end,
157
+ newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
158
+ start: call.start
159
+ });
160
+ }
161
+ return addMissingImports(applyReplacements(code, replacements), ctx.allNeededImports, EXTRA_IMPORTS);
162
+ };
163
+ export { transformReactiveObjects };
@@ -1,4 +1,4 @@
1
- import type { Bindings } from '../../../types.js';
1
+ import type { Bindings } from '../../types.js';
2
2
  import ts from 'typescript';
3
3
  declare const transformReactivePrimitives: (sourceFile: ts.SourceFile, bindings: Bindings) => string;
4
4
  export { transformReactivePrimitives };