@esportsplus/reactivity 0.23.0 → 0.23.1

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 (39) hide show
  1. package/build/transformer/detector.js +38 -0
  2. package/build/transformer/{core/index.d.ts → index.d.ts} +1 -1
  3. package/build/transformer/plugins/esbuild.js +3 -2
  4. package/build/transformer/plugins/tsc.js +1 -1
  5. package/build/transformer/plugins/vite.js +2 -2
  6. package/build/transformer/transforms/auto-dispose.js +119 -0
  7. package/build/transformer/{core/transforms → transforms}/reactive-array.d.ts +1 -1
  8. package/build/transformer/transforms/reactive-array.js +93 -0
  9. package/build/transformer/{core/transforms → transforms}/reactive-object.d.ts +1 -1
  10. package/build/transformer/transforms/reactive-object.js +164 -0
  11. package/build/transformer/{core/transforms → transforms}/reactive-primitives.d.ts +1 -1
  12. package/build/transformer/transforms/reactive-primitives.js +335 -0
  13. package/build/transformer/{core/transforms → transforms}/utilities.d.ts +1 -2
  14. package/build/transformer/transforms/utilities.js +73 -0
  15. package/package.json +8 -12
  16. package/src/transformer/detector.ts +65 -0
  17. package/src/transformer/{core/index.ts → index.ts} +1 -5
  18. package/src/transformer/plugins/esbuild.ts +3 -2
  19. package/src/transformer/plugins/tsc.ts +1 -1
  20. package/src/transformer/plugins/vite.ts +2 -4
  21. package/src/transformer/transforms/auto-dispose.ts +191 -0
  22. package/src/transformer/transforms/reactive-array.ts +143 -0
  23. package/src/transformer/{core/transforms → transforms}/reactive-object.ts +101 -92
  24. package/src/transformer/transforms/reactive-primitives.ts +461 -0
  25. package/src/transformer/transforms/utilities.ts +119 -0
  26. package/build/transformer/core/detector.js +0 -6
  27. package/build/transformer/core/transforms/auto-dispose.js +0 -116
  28. package/build/transformer/core/transforms/reactive-array.js +0 -89
  29. package/build/transformer/core/transforms/reactive-object.js +0 -155
  30. package/build/transformer/core/transforms/reactive-primitives.js +0 -325
  31. package/build/transformer/core/transforms/utilities.js +0 -57
  32. package/src/transformer/core/detector.ts +0 -12
  33. package/src/transformer/core/transforms/auto-dispose.ts +0 -194
  34. package/src/transformer/core/transforms/reactive-array.ts +0 -140
  35. package/src/transformer/core/transforms/reactive-primitives.ts +0 -459
  36. package/src/transformer/core/transforms/utilities.ts +0 -95
  37. /package/build/transformer/{core/detector.d.ts → detector.d.ts} +0 -0
  38. /package/build/transformer/{core/index.js → index.js} +0 -0
  39. /package/build/transformer/{core/transforms → transforms}/auto-dispose.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 };
@@ -1,4 +1,4 @@
1
- import type { TransformOptions, TransformResult } from '../../types.js';
1
+ import type { TransformOptions, TransformResult } from '../types.js';
2
2
  import { mightNeedTransform } from './detector.js';
3
3
  import ts from 'typescript';
4
4
  declare const createTransformer: (options?: TransformOptions) => ts.TransformerFactory<ts.SourceFile>;
@@ -1,11 +1,12 @@
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
5
  export default (options) => {
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;
@@ -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,6 +1,6 @@
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
4
  export default (options) => {
5
5
  return {
6
6
  enforce: 'pre',
@@ -0,0 +1,119 @@
1
+ import { uid, TRAILING_SEMICOLON } from '@esportsplus/typescript/transformer';
2
+ import { applyReplacements } from './utilities.js';
3
+ import ts from 'typescript';
4
+ function processFunction(node, sourceFile, edits) {
5
+ if (!node.body || !ts.isBlock(node.body)) {
6
+ return;
7
+ }
8
+ let ctx = {
9
+ disposables: [],
10
+ effectsToCapture: [],
11
+ parentBody: node.body,
12
+ returnStatement: null
13
+ };
14
+ visitBody(ctx, node.body);
15
+ if (ctx.disposables.length === 0 || !ctx.returnStatement || !ctx.returnStatement.expression) {
16
+ return;
17
+ }
18
+ let cleanupFn = ctx.returnStatement.expression;
19
+ if (!cleanupFn.body) {
20
+ return;
21
+ }
22
+ let disposeStatements = [];
23
+ for (let i = ctx.disposables.length - 1; i >= 0; i--) {
24
+ let d = ctx.disposables[i];
25
+ if (d.type === 'reactive') {
26
+ disposeStatements.push(`${d.name}.dispose();`);
27
+ }
28
+ else {
29
+ disposeStatements.push(`${d.name}();`);
30
+ }
31
+ }
32
+ let disposeCode = disposeStatements.join('\n');
33
+ if (ts.isBlock(cleanupFn.body)) {
34
+ edits.push({
35
+ cleanupBodyEnd: cleanupFn.body.statements[0]?.pos ?? cleanupFn.body.end - 1,
36
+ cleanupBodyStart: cleanupFn.body.pos + 1,
37
+ disposeCode,
38
+ effectsToCapture: ctx.effectsToCapture
39
+ });
40
+ }
41
+ else {
42
+ edits.push({
43
+ cleanupBodyEnd: cleanupFn.body.end,
44
+ cleanupBodyStart: cleanupFn.body.pos,
45
+ disposeCode: `{ ${disposeCode}\n return ${cleanupFn.body.getText(sourceFile)}; }`,
46
+ effectsToCapture: ctx.effectsToCapture
47
+ });
48
+ }
49
+ }
50
+ function visitBody(ctx, node) {
51
+ if (ts.isVariableDeclaration(node) &&
52
+ ts.isIdentifier(node.name) &&
53
+ node.initializer &&
54
+ ts.isCallExpression(node.initializer) &&
55
+ ts.isIdentifier(node.initializer.expression) &&
56
+ node.initializer.expression.text === 'reactive') {
57
+ ctx.disposables.push({ name: node.name.text, type: 'reactive' });
58
+ }
59
+ if (ts.isCallExpression(node) &&
60
+ ts.isIdentifier(node.expression) &&
61
+ node.expression.text === 'effect') {
62
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
63
+ ctx.disposables.push({ name: node.parent.name.text, type: 'effect' });
64
+ }
65
+ else if (ts.isExpressionStatement(node.parent)) {
66
+ let name = uid('effect');
67
+ ctx.effectsToCapture.push({
68
+ end: node.parent.end,
69
+ name,
70
+ start: node.parent.pos
71
+ });
72
+ ctx.disposables.push({ name, type: 'effect' });
73
+ }
74
+ }
75
+ if (ts.isReturnStatement(node) &&
76
+ node.expression &&
77
+ (ts.isArrowFunction(node.expression) || ts.isFunctionExpression(node.expression)) &&
78
+ node.parent === ctx.parentBody) {
79
+ ctx.returnStatement = node;
80
+ }
81
+ ts.forEachChild(node, n => visitBody(ctx, n));
82
+ }
83
+ function visitMain(ctx, node) {
84
+ if (ts.isFunctionDeclaration(node) ||
85
+ ts.isFunctionExpression(node) ||
86
+ ts.isArrowFunction(node)) {
87
+ processFunction(node, ctx.sourceFile, ctx.edits);
88
+ }
89
+ ts.forEachChild(node, n => visitMain(ctx, n));
90
+ }
91
+ const injectAutoDispose = (sourceFile) => {
92
+ let code = sourceFile.getFullText(), ctx = {
93
+ edits: [],
94
+ sourceFile
95
+ };
96
+ visitMain(ctx, sourceFile);
97
+ if (ctx.edits.length === 0) {
98
+ return code;
99
+ }
100
+ let replacements = [];
101
+ for (let i = 0, n = ctx.edits.length; i < n; i++) {
102
+ let edit = ctx.edits[i], effects = edit.effectsToCapture;
103
+ for (let j = 0, m = effects.length; j < m; j++) {
104
+ let effect = effects[j], original = code.substring(effect.start, effect.end).trim();
105
+ replacements.push({
106
+ end: effect.end,
107
+ newText: `const ${effect.name} = ${original.replace(TRAILING_SEMICOLON, '')}`,
108
+ start: effect.start
109
+ });
110
+ }
111
+ replacements.push({
112
+ end: edit.cleanupBodyEnd,
113
+ newText: `\n${edit.disposeCode}`,
114
+ start: edit.cleanupBodyStart
115
+ });
116
+ }
117
+ return applyReplacements(code, replacements);
118
+ };
119
+ export { injectAutoDispose };
@@ -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,164 @@
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
+ let disposeBody = disposeStatements.length > 0 ? disposeStatements.join('\n') : '';
52
+ return `
53
+ class ${className} {
54
+ ${fields.join('\n')}
55
+ ${accessors.join('\n')}
56
+
57
+ dispose() {
58
+ ${disposeBody}
59
+ }
60
+ }
61
+ `;
62
+ }
63
+ function visit(ctx, node) {
64
+ if (ts.isImportDeclaration(node)) {
65
+ ctx.lastImportEnd = node.end;
66
+ if (ts.isStringLiteral(node.moduleSpecifier) &&
67
+ node.moduleSpecifier.text.includes('@esportsplus/reactivity')) {
68
+ let clause = node.importClause;
69
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
70
+ let elements = clause.namedBindings.elements;
71
+ for (let i = 0, n = elements.length; i < n; i++) {
72
+ if (elements[i].name.text === 'reactive') {
73
+ ctx.hasReactiveImport = true;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ if (ctx.hasReactiveImport &&
81
+ ts.isCallExpression(node) &&
82
+ ts.isIdentifier(node.expression) &&
83
+ node.expression.text === 'reactive') {
84
+ let arg = node.arguments[0];
85
+ if (arg && ts.isObjectLiteralExpression(arg)) {
86
+ let varName = null;
87
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
88
+ varName = node.parent.name.text;
89
+ ctx.bindings.set(varName, 'object');
90
+ }
91
+ let needsImports = new Set(), properties = [];
92
+ needsImports.add('REACTIVE_OBJECT');
93
+ let props = arg.properties;
94
+ for (let i = 0, n = props.length; i < n; i++) {
95
+ let prop = props[i];
96
+ if (ts.isSpreadAssignment(prop)) {
97
+ ts.forEachChild(node, n => visit(ctx, n));
98
+ return;
99
+ }
100
+ let analyzed = analyzeProperty(prop, ctx.sourceFile);
101
+ if (!analyzed) {
102
+ ts.forEachChild(node, n => visit(ctx, n));
103
+ return;
104
+ }
105
+ properties.push(analyzed);
106
+ if (analyzed.type === 'signal') {
107
+ needsImports.add('read');
108
+ needsImports.add('set');
109
+ needsImports.add('signal');
110
+ }
111
+ else if (analyzed.type === 'array') {
112
+ needsImports.add('ReactiveArray');
113
+ if (varName) {
114
+ ctx.bindings.set(`${varName}.${analyzed.key}`, 'array');
115
+ }
116
+ }
117
+ else if (analyzed.type === 'computed') {
118
+ needsImports.add('computed');
119
+ needsImports.add('dispose');
120
+ needsImports.add('read');
121
+ }
122
+ }
123
+ needsImports.forEach(imp => ctx.allNeededImports.add(imp));
124
+ ctx.calls.push({
125
+ end: node.end,
126
+ generatedClass: buildClassCode(uid('ReactiveObject'), properties),
127
+ needsImports,
128
+ start: node.pos,
129
+ varName
130
+ });
131
+ }
132
+ }
133
+ ts.forEachChild(node, n => visit(ctx, n));
134
+ }
135
+ const transformReactiveObjects = (sourceFile, bindings) => {
136
+ let code = sourceFile.getFullText(), ctx = {
137
+ allNeededImports: new Set(),
138
+ bindings,
139
+ calls: [],
140
+ hasReactiveImport: false,
141
+ lastImportEnd: 0,
142
+ sourceFile
143
+ };
144
+ visit(ctx, sourceFile);
145
+ if (ctx.calls.length === 0) {
146
+ return code;
147
+ }
148
+ let replacements = [];
149
+ replacements.push({
150
+ end: ctx.lastImportEnd,
151
+ newText: code.substring(0, ctx.lastImportEnd) + '\n' + ctx.calls.map(c => c.generatedClass).join('\n') + '\n',
152
+ start: 0
153
+ });
154
+ for (let i = 0, n = ctx.calls.length; i < n; i++) {
155
+ let call = ctx.calls[i], classMatch = call.generatedClass.match(CLASS_NAME_REGEX);
156
+ replacements.push({
157
+ end: call.end,
158
+ newText: ` new ${classMatch ? classMatch[1] : 'ReactiveObject'}()`,
159
+ start: call.start
160
+ });
161
+ }
162
+ return addMissingImports(applyReplacements(code, replacements), ctx.allNeededImports, EXTRA_IMPORTS);
163
+ };
164
+ 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 };