workstar-compiler 0.1.1 → 0.2.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 +5 -1
- package/bin/workstar-compile.mjs +15 -1
- package/dist/src/compat-rules.d.ts +2 -0
- package/dist/src/compat-rules.js +4 -0
- package/dist/src/compat.d.ts +5 -0
- package/dist/src/compat.js +12 -0
- package/dist/src/index.js +8 -3
- package/dist/src/project.d.ts +15 -1
- package/dist/src/project.js +56 -4
- package/dist/src/react-compat.d.ts +6 -0
- package/dist/src/react-compat.js +369 -0
- package/dist/src/react-entry-compat.d.ts +2 -0
- package/dist/src/react-entry-compat.js +105 -0
- package/dist/src/react-import-resolution.d.ts +2 -0
- package/dist/src/react-import-resolution.js +15 -0
- package/dist/src/vite.d.ts +4 -0
- package/dist/src/vite.js +109 -2
- package/dist/src/vue-compat.d.ts +2 -0
- package/dist/src/vue-compat.js +168 -0
- package/dist/src/vue-script-compat.d.ts +8 -0
- package/dist/src/vue-script-compat.js +244 -0
- package/package.json +6 -2
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { reject } from './compat-rules.js';
|
|
3
|
+
function primitive(node) {
|
|
4
|
+
return (ts.isNumericLiteral(node) ||
|
|
5
|
+
ts.isStringLiteral(node) ||
|
|
6
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
7
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
8
|
+
(ts.isPrefixUnaryExpression(node) &&
|
|
9
|
+
node.operator === ts.SyntaxKind.MinusToken &&
|
|
10
|
+
ts.isNumericLiteral(node.operand)));
|
|
11
|
+
}
|
|
12
|
+
function propsCall(statement, file, filename) {
|
|
13
|
+
let call;
|
|
14
|
+
if (ts.isExpressionStatement(statement) &&
|
|
15
|
+
ts.isCallExpression(statement.expression)) {
|
|
16
|
+
call = statement.expression;
|
|
17
|
+
}
|
|
18
|
+
else if (ts.isVariableStatement(statement) &&
|
|
19
|
+
statement.declarationList.declarations.length === 1) {
|
|
20
|
+
const declaration = statement.declarationList.declarations[0];
|
|
21
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
22
|
+
declaration.name.text === 'props' &&
|
|
23
|
+
!declaration.type &&
|
|
24
|
+
declaration.initializer &&
|
|
25
|
+
ts.isCallExpression(declaration.initializer)) {
|
|
26
|
+
call = declaration.initializer;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!call || call.expression.getText(file) !== 'defineProps')
|
|
30
|
+
return;
|
|
31
|
+
const type = call.typeArguments?.[0];
|
|
32
|
+
if (call.arguments.length ||
|
|
33
|
+
call.typeArguments?.length !== 1 ||
|
|
34
|
+
!type ||
|
|
35
|
+
!ts.isTypeLiteralNode(type)) {
|
|
36
|
+
reject(filename, 'defineProps type');
|
|
37
|
+
}
|
|
38
|
+
return type;
|
|
39
|
+
}
|
|
40
|
+
function vueImport(statement, filename) {
|
|
41
|
+
const named = statement.importClause?.namedBindings;
|
|
42
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
43
|
+
statement.moduleSpecifier.text !== 'vue' ||
|
|
44
|
+
statement.importClause?.isTypeOnly ||
|
|
45
|
+
!named ||
|
|
46
|
+
!ts.isNamedImports(named) ||
|
|
47
|
+
named.elements.length === 0 ||
|
|
48
|
+
named.elements.some((entry) => entry.propertyName || !['ref', 'reactive'].includes(entry.name.text))) {
|
|
49
|
+
reject(filename, 'script import');
|
|
50
|
+
}
|
|
51
|
+
const apis = new Set(named.elements.map((entry) => entry.name.text));
|
|
52
|
+
if (apis.size !== named.elements.length)
|
|
53
|
+
reject(filename, 'script import');
|
|
54
|
+
return apis;
|
|
55
|
+
}
|
|
56
|
+
function stateDeclaration(statement, file, filename) {
|
|
57
|
+
if (!ts.isVariableStatement(statement))
|
|
58
|
+
return;
|
|
59
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0 ||
|
|
60
|
+
statement.modifiers?.length ||
|
|
61
|
+
statement.declarationList.declarations.length !== 1) {
|
|
62
|
+
reject(filename, 'state declaration');
|
|
63
|
+
}
|
|
64
|
+
const declaration = statement.declarationList.declarations[0];
|
|
65
|
+
const initializer = declaration.initializer;
|
|
66
|
+
if (!ts.isIdentifier(declaration.name) ||
|
|
67
|
+
declaration.name.text === 'props' ||
|
|
68
|
+
['__workstarSignal', '__workstarStore'].includes(declaration.name.text) ||
|
|
69
|
+
declaration.type ||
|
|
70
|
+
!initializer ||
|
|
71
|
+
!ts.isCallExpression(initializer) ||
|
|
72
|
+
initializer.arguments.length !== 1) {
|
|
73
|
+
reject(filename, 'primitive state declaration');
|
|
74
|
+
}
|
|
75
|
+
const kind = initializer.expression.getText(file);
|
|
76
|
+
if (kind === 'reactive') {
|
|
77
|
+
const record = initializer.arguments[0];
|
|
78
|
+
if (initializer.typeArguments?.length ||
|
|
79
|
+
!ts.isObjectLiteralExpression(record) ||
|
|
80
|
+
record.properties.length === 0) {
|
|
81
|
+
reject(filename, 'flat reactive declaration');
|
|
82
|
+
}
|
|
83
|
+
const fields = new Set();
|
|
84
|
+
for (const property of record.properties) {
|
|
85
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
86
|
+
!ts.isIdentifier(property.name) ||
|
|
87
|
+
!primitive(property.initializer) ||
|
|
88
|
+
fields.has(property.name.text) ||
|
|
89
|
+
property.name.text === '__proto__') {
|
|
90
|
+
reject(filename, 'flat reactive declaration');
|
|
91
|
+
}
|
|
92
|
+
fields.add(property.name.text);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
name: declaration.name.text,
|
|
96
|
+
kind,
|
|
97
|
+
fields,
|
|
98
|
+
code: `const ${declaration.name.text} = __workstarStore(${record.getText(file)});`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (kind !== 'ref' ||
|
|
102
|
+
!primitive(initializer.arguments[0]) ||
|
|
103
|
+
(initializer.typeArguments?.length ?? 0) > 1) {
|
|
104
|
+
reject(filename, 'primitive ref declaration');
|
|
105
|
+
}
|
|
106
|
+
const type = initializer.typeArguments?.[0];
|
|
107
|
+
return {
|
|
108
|
+
name: declaration.name.text,
|
|
109
|
+
kind,
|
|
110
|
+
code: 'const ' +
|
|
111
|
+
declaration.name.text +
|
|
112
|
+
' = __workstarSignal' +
|
|
113
|
+
(type ? '<' + type.getText(file) + '>' : '') +
|
|
114
|
+
'(' +
|
|
115
|
+
initializer.arguments[0].getText(file) +
|
|
116
|
+
');',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function writableState(node, refs, stores) {
|
|
120
|
+
if (!ts.isPropertyAccessExpression(node) || !ts.isIdentifier(node.expression))
|
|
121
|
+
return false;
|
|
122
|
+
const owner = node.expression.text;
|
|
123
|
+
return ((refs.has(owner) && node.name.text === 'value') ||
|
|
124
|
+
stores.get(owner)?.has(node.name.text) === true);
|
|
125
|
+
}
|
|
126
|
+
function stateWrite(expression, refs, stores) {
|
|
127
|
+
if (ts.isPostfixUnaryExpression(expression) ||
|
|
128
|
+
ts.isPrefixUnaryExpression(expression)) {
|
|
129
|
+
return ((expression.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
130
|
+
expression.operator === ts.SyntaxKind.MinusMinusToken) &&
|
|
131
|
+
writableState(expression.operand, refs, stores));
|
|
132
|
+
}
|
|
133
|
+
return (ts.isBinaryExpression(expression) &&
|
|
134
|
+
writableState(expression.left, refs, stores) &&
|
|
135
|
+
[
|
|
136
|
+
ts.SyntaxKind.EqualsToken,
|
|
137
|
+
ts.SyntaxKind.PlusEqualsToken,
|
|
138
|
+
ts.SyntaxKind.MinusEqualsToken,
|
|
139
|
+
].includes(expression.operatorToken.kind) &&
|
|
140
|
+
primitive(expression.right));
|
|
141
|
+
}
|
|
142
|
+
function stateHandler(statement, refs, stores, file, filename) {
|
|
143
|
+
if (!statement.name ||
|
|
144
|
+
!statement.body ||
|
|
145
|
+
statement.modifiers?.length ||
|
|
146
|
+
statement.asteriskToken ||
|
|
147
|
+
statement.typeParameters?.length ||
|
|
148
|
+
statement.parameters.length ||
|
|
149
|
+
statement.body.statements.length === 0 ||
|
|
150
|
+
statement.body.statements.some((entry) => !ts.isExpressionStatement(entry) ||
|
|
151
|
+
!stateWrite(entry.expression, refs, stores))) {
|
|
152
|
+
reject(filename, 'state event handler');
|
|
153
|
+
}
|
|
154
|
+
return { name: statement.name.text, code: statement.getText(file) };
|
|
155
|
+
}
|
|
156
|
+
/** Translate primitive Vue refs, flat reactive records, props, and their handlers. */
|
|
157
|
+
export function convertVueScript(script, filename) {
|
|
158
|
+
const diagnostics = ts.transpileModule(script, {
|
|
159
|
+
fileName: filename,
|
|
160
|
+
reportDiagnostics: true,
|
|
161
|
+
}).diagnostics ?? [];
|
|
162
|
+
if (diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
|
|
163
|
+
reject(filename, 'invalid script setup syntax');
|
|
164
|
+
}
|
|
165
|
+
const file = ts.createSourceFile(filename, script, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
166
|
+
let props = '';
|
|
167
|
+
let importedApis = new Set();
|
|
168
|
+
let hasImport = false;
|
|
169
|
+
const names = new Set(['props', '__workstarSignal', '__workstarStore']);
|
|
170
|
+
const refs = new Set();
|
|
171
|
+
const stores = new Map();
|
|
172
|
+
const setup = [];
|
|
173
|
+
const handlers = [];
|
|
174
|
+
for (const statement of file.statements) {
|
|
175
|
+
if (ts.isEmptyStatement(statement))
|
|
176
|
+
continue;
|
|
177
|
+
const type = propsCall(statement, file, filename);
|
|
178
|
+
if (type) {
|
|
179
|
+
if (props)
|
|
180
|
+
reject(filename, 'duplicate defineProps');
|
|
181
|
+
props = 'export type Props = ' + type.getText(file) + ';';
|
|
182
|
+
for (const member of type.members) {
|
|
183
|
+
if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name))
|
|
184
|
+
reject(filename, 'Props property');
|
|
185
|
+
if (names.has(member.name.text))
|
|
186
|
+
reject(filename, 'reserved or duplicate Props property');
|
|
187
|
+
names.add(member.name.text);
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ts.isImportDeclaration(statement)) {
|
|
192
|
+
if (hasImport)
|
|
193
|
+
reject(filename, 'script import');
|
|
194
|
+
importedApis = vueImport(statement, filename);
|
|
195
|
+
hasImport = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ts.isVariableStatement(statement)) {
|
|
199
|
+
const declaration = stateDeclaration(statement, file, filename);
|
|
200
|
+
if (!declaration || names.has(declaration.name))
|
|
201
|
+
reject(filename, 'state declaration');
|
|
202
|
+
names.add(declaration.name);
|
|
203
|
+
if (declaration.kind === 'ref')
|
|
204
|
+
refs.add(declaration.name);
|
|
205
|
+
else
|
|
206
|
+
stores.set(declaration.name, declaration.fields);
|
|
207
|
+
setup.push(declaration.code);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
211
|
+
handlers.push(statement);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
reject(filename, 'script setup logic');
|
|
215
|
+
}
|
|
216
|
+
const usedApis = new Set();
|
|
217
|
+
if (refs.size)
|
|
218
|
+
usedApis.add('ref');
|
|
219
|
+
if (stores.size)
|
|
220
|
+
usedApis.add('reactive');
|
|
221
|
+
if (usedApis.size !== importedApis.size ||
|
|
222
|
+
[...usedApis].some((api) => !importedApis.has(api))) {
|
|
223
|
+
reject(filename, 'unsupported or unused Vue reactivity import');
|
|
224
|
+
}
|
|
225
|
+
for (const handler of handlers) {
|
|
226
|
+
const converted = stateHandler(handler, refs, stores, file, filename);
|
|
227
|
+
if (names.has(converted.name))
|
|
228
|
+
reject(filename, 'handler name');
|
|
229
|
+
names.add(converted.name);
|
|
230
|
+
setup.push(converted.code);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
props,
|
|
234
|
+
setup: (refs.size
|
|
235
|
+
? "import { signal as __workstarSignal } from 'workstar';\n"
|
|
236
|
+
: '') +
|
|
237
|
+
(stores.size
|
|
238
|
+
? "import { store as __workstarStore } from 'workstar';\n"
|
|
239
|
+
: '') +
|
|
240
|
+
setup.join('\n'),
|
|
241
|
+
refs,
|
|
242
|
+
stores,
|
|
243
|
+
};
|
|
244
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workstar-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Component compiler and Vite plugin for Workstar applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"./vite": {
|
|
25
25
|
"types": "./dist/src/vite.d.ts",
|
|
26
26
|
"import": "./dist/src/vite.js"
|
|
27
|
+
},
|
|
28
|
+
"./compat": {
|
|
29
|
+
"types": "./dist/src/compat.d.ts",
|
|
30
|
+
"import": "./dist/src/compat.js"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
29
33
|
"bin": {
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
"typescript": "^6.0.3"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
|
45
|
-
"vite": "^8.0.0"
|
|
49
|
+
"vite": "^7.0.0 || ^8.0.0"
|
|
46
50
|
},
|
|
47
51
|
"peerDependenciesMeta": {
|
|
48
52
|
"vite": {
|