workstar-compiler 0.1.0 → 0.2.0-beta.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 +12 -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/component-script.d.ts +11 -3
- package/dist/src/component-script.js +93 -24
- package/dist/src/control-elements.d.ts +5 -0
- package/dist/src/control-elements.js +55 -20
- package/dist/src/errors.d.ts +9 -1
- package/dist/src/errors.js +6 -2
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +114 -40
- 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/source-map.d.ts +7 -0
- package/dist/src/source-map.js +50 -0
- package/dist/src/source-origin.d.ts +14 -0
- package/dist/src/source-origin.js +41 -0
- package/dist/src/styles.d.ts +5 -0
- package/dist/src/styles.js +41 -18
- package/dist/src/vite.d.ts +4 -0
- package/dist/src/vite.js +150 -13
- 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 +7 -2
package/dist/src/styles.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import postcss from 'postcss';
|
|
3
3
|
import selectorParser from 'postcss-selector-parser';
|
|
4
|
+
export class StyleCompileError extends Error {
|
|
5
|
+
line;
|
|
6
|
+
column;
|
|
7
|
+
constructor(message, line, column) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.line = line;
|
|
10
|
+
this.column = column;
|
|
11
|
+
this.name = 'StyleCompileError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
function scopeSelector(selector, attribute) {
|
|
5
15
|
const scopeNode = selectorParser().astSync(`:where([${attribute}])`).first
|
|
6
16
|
?.first;
|
|
@@ -34,23 +44,36 @@ function scopeSelector(selector, attribute) {
|
|
|
34
44
|
}
|
|
35
45
|
/** Compile CSS at build time; styles never depend on client hydration. */
|
|
36
46
|
export function compileStyle(css, filename, global = false) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
try {
|
|
48
|
+
const root = postcss.parse(css, { from: filename });
|
|
49
|
+
root.walkAtRules((rule) => {
|
|
50
|
+
if (rule.name === 'import' || rule.name === 'charset') {
|
|
51
|
+
throw rule.error(`@${rule.name} belongs in a global stylesheet.`);
|
|
52
|
+
}
|
|
53
|
+
if (/keyframes$/i.test(rule.name) && !global) {
|
|
54
|
+
throw rule.error('Put @keyframes in <style global>.');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (global)
|
|
58
|
+
return { css: root.toString().trim() };
|
|
59
|
+
const scopeAttribute = `data-workstar-${createHash('sha256')
|
|
60
|
+
.update(filename)
|
|
61
|
+
.digest('hex')
|
|
62
|
+
.slice(0, 10)}`;
|
|
63
|
+
root.walkRules((rule) => {
|
|
64
|
+
try {
|
|
65
|
+
rule.selector = scopeSelector(rule.selector, scopeAttribute);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw rule.error(error instanceof Error ? error.message : String(error));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return { css: root.toString().trim(), scopeAttribute };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof postcss.CssSyntaxError) {
|
|
75
|
+
throw new StyleCompileError(error.reason, error.line ?? 1, error.column ?? 1);
|
|
44
76
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return { css: root.toString().trim() };
|
|
48
|
-
const scopeAttribute = `data-workstar-${createHash('sha256')
|
|
49
|
-
.update(filename)
|
|
50
|
-
.digest('hex')
|
|
51
|
-
.slice(0, 10)}`;
|
|
52
|
-
root.walkRules((rule) => {
|
|
53
|
-
rule.selector = scopeSelector(rule.selector, scopeAttribute);
|
|
54
|
-
});
|
|
55
|
-
return { css: root.toString().trim(), scopeAttribute };
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
56
79
|
}
|
package/dist/src/vite.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
export interface WorkstarPluginOptions {
|
|
3
3
|
source?: string;
|
|
4
|
+
/** Compile ordinary TSX/Vue imports within source, without ?workstar. */
|
|
5
|
+
foreign?: 'explicit' | 'automatic' | 'runtime';
|
|
6
|
+
/** Optional absolute runtime directory for isolated migration builds. */
|
|
7
|
+
runtimeImportSource?: string;
|
|
4
8
|
}
|
|
5
9
|
/** Compile authored components as Vite modules without writing into src. */
|
|
6
10
|
export declare function workstar(options?: WorkstarPluginOptions): Plugin;
|
package/dist/src/vite.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
-
import ts from 'typescript';
|
|
2
|
+
import { dirname, extname, isAbsolute, relative, resolve, sep, } from 'node:path';
|
|
4
3
|
import { compileComponentParts } from './index.js';
|
|
4
|
+
import { convertForeignComponent } from './compat.js';
|
|
5
|
+
import { convertReactRootEntry } from './react-entry-compat.js';
|
|
6
|
+
import { reactComponentExportName } from './react-compat.js';
|
|
7
|
+
import { resolveReactComponentImport } from './react-import-resolution.js';
|
|
8
|
+
import { transpileComponent } from './source-map.js';
|
|
5
9
|
/** Compile authored components as Vite modules without writing into src. */
|
|
6
10
|
export function workstar(options = {}) {
|
|
7
11
|
let sourceDirectory;
|
|
12
|
+
let development = false;
|
|
8
13
|
const styleSuffix = '.css?workstar-style';
|
|
14
|
+
const foreignPrefix = '\0workstar-foreign:';
|
|
15
|
+
const foreignStylePrefix = '\0workstar-foreign-style:';
|
|
16
|
+
const foreignStylePublic = 'virtual:workstar-foreign-style:';
|
|
17
|
+
const foreignStyles = new Map();
|
|
18
|
+
const compiled = new Map();
|
|
9
19
|
function isAuthoredComponent(filename) {
|
|
10
20
|
const localPath = relative(sourceDirectory, filename);
|
|
11
21
|
return (extname(localPath) === '.workstar' &&
|
|
@@ -13,19 +23,102 @@ export function workstar(options = {}) {
|
|
|
13
23
|
!localPath.startsWith(`..${sep}`) &&
|
|
14
24
|
!isAbsolute(localPath));
|
|
15
25
|
}
|
|
26
|
+
function isForeignSource(filename) {
|
|
27
|
+
const localPath = relative(sourceDirectory, filename);
|
|
28
|
+
return ((filename.endsWith('.tsx') || filename.endsWith('.vue')) &&
|
|
29
|
+
localPath !== '..' &&
|
|
30
|
+
!localPath.startsWith(`..${sep}`) &&
|
|
31
|
+
!isAbsolute(localPath));
|
|
32
|
+
}
|
|
33
|
+
function rootComponents(modules) {
|
|
34
|
+
const roots = new Set();
|
|
35
|
+
const visited = new Set();
|
|
36
|
+
function visit(module) {
|
|
37
|
+
if (visited.has(module))
|
|
38
|
+
return;
|
|
39
|
+
visited.add(module);
|
|
40
|
+
const parents = [...module.importers].filter((importer) => importer.id && isAuthoredComponent(importer.id.split('?', 1)[0]));
|
|
41
|
+
if (parents.length === 0)
|
|
42
|
+
roots.add(module);
|
|
43
|
+
else
|
|
44
|
+
parents.forEach(visit);
|
|
45
|
+
}
|
|
46
|
+
modules.forEach(visit);
|
|
47
|
+
return [...roots];
|
|
48
|
+
}
|
|
49
|
+
function compileForeignSource(source, filename) {
|
|
50
|
+
const converted = convertForeignComponent(source, filename, {
|
|
51
|
+
resolveReactImport: (specifier) => resolveReactComponentImport(filename, specifier),
|
|
52
|
+
});
|
|
53
|
+
const namedExport = filename.endsWith('.tsx')
|
|
54
|
+
? reactComponentExportName(source, filename)
|
|
55
|
+
: undefined;
|
|
56
|
+
const generated = compileComponentParts(converted, `${filename}.workstar`, {
|
|
57
|
+
cssImport: foreignStylePublic + encodeURIComponent(filename) + '.css',
|
|
58
|
+
});
|
|
59
|
+
foreignStyles.set(filename, generated.css);
|
|
60
|
+
const output = transpileComponent(`${generated.code}\nexport { render as ${namedExport ?? 'default'} };\n`, converted, filename, generated.origins);
|
|
61
|
+
return { code: output.code, map: null };
|
|
62
|
+
}
|
|
16
63
|
return {
|
|
17
64
|
name: 'workstar',
|
|
18
65
|
enforce: 'pre',
|
|
66
|
+
config() {
|
|
67
|
+
if (options.foreign !== 'runtime')
|
|
68
|
+
return;
|
|
69
|
+
const runtime = options.runtimeImportSource ?? 'workstar/compat/react';
|
|
70
|
+
const react = isAbsolute(runtime) ? `${runtime}/index.js` : runtime;
|
|
71
|
+
const client = isAbsolute(runtime)
|
|
72
|
+
? `${runtime}/client.js`
|
|
73
|
+
: `${runtime}/client`;
|
|
74
|
+
const router = isAbsolute(runtime)
|
|
75
|
+
? resolve(runtime, '../react-router/index.js')
|
|
76
|
+
: 'workstar/compat/react-router';
|
|
77
|
+
return {
|
|
78
|
+
esbuild: {
|
|
79
|
+
jsx: 'automatic',
|
|
80
|
+
jsxImportSource: runtime,
|
|
81
|
+
},
|
|
82
|
+
resolve: {
|
|
83
|
+
alias: [
|
|
84
|
+
{ find: 'react-router', replacement: router },
|
|
85
|
+
{ find: 'react-dom/client', replacement: client },
|
|
86
|
+
{ find: 'react', replacement: react },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
19
91
|
configResolved(config) {
|
|
20
92
|
sourceDirectory = resolve(config.root, options.source ?? 'src');
|
|
93
|
+
development = config.command === 'serve';
|
|
21
94
|
},
|
|
22
|
-
resolveId(id) {
|
|
95
|
+
resolveId(id, importer) {
|
|
96
|
+
if (id.startsWith(foreignStylePublic)) {
|
|
97
|
+
const encoded = id.slice(foreignStylePublic.length, -'.css'.length);
|
|
98
|
+
return foreignStylePrefix + decodeURIComponent(encoded) + '.css';
|
|
99
|
+
}
|
|
100
|
+
if (id.endsWith('?workstar') && importer && id.startsWith('.')) {
|
|
101
|
+
const importerPath = importer.startsWith(foreignPrefix)
|
|
102
|
+
? importer.slice(foreignPrefix.length)
|
|
103
|
+
: importer.split('?', 1)[0];
|
|
104
|
+
const filename = resolve(dirname(importerPath), id.slice(0, -'?workstar'.length));
|
|
105
|
+
return isForeignSource(filename) ? foreignPrefix + filename : null;
|
|
106
|
+
}
|
|
23
107
|
if (!id.endsWith(styleSuffix))
|
|
24
108
|
return null;
|
|
25
109
|
const filename = id.slice(0, -styleSuffix.length);
|
|
26
110
|
return isAuthoredComponent(filename) ? id : null;
|
|
27
111
|
},
|
|
28
112
|
async load(id) {
|
|
113
|
+
if (id.startsWith(foreignStylePrefix)) {
|
|
114
|
+
return (foreignStyles.get(id.slice(foreignStylePrefix.length, -'.css'.length)) ?? null);
|
|
115
|
+
}
|
|
116
|
+
if (id.startsWith(foreignPrefix)) {
|
|
117
|
+
const filename = id.slice(foreignPrefix.length);
|
|
118
|
+
this.addWatchFile(filename);
|
|
119
|
+
const source = await readFile(filename, 'utf8');
|
|
120
|
+
return compileForeignSource(source, filename);
|
|
121
|
+
}
|
|
29
122
|
if (!id.endsWith(styleSuffix))
|
|
30
123
|
return null;
|
|
31
124
|
const filename = id.slice(0, -styleSuffix.length);
|
|
@@ -36,31 +129,75 @@ export function workstar(options = {}) {
|
|
|
36
129
|
},
|
|
37
130
|
transform(source, id) {
|
|
38
131
|
const filename = id.split('?', 1)[0];
|
|
132
|
+
if (options.foreign === 'automatic' && isForeignSource(filename)) {
|
|
133
|
+
const entry = filename.endsWith('.tsx')
|
|
134
|
+
? convertReactRootEntry(source, filename)
|
|
135
|
+
: undefined;
|
|
136
|
+
if (entry)
|
|
137
|
+
return { code: entry, map: null };
|
|
138
|
+
return compileForeignSource(source, filename);
|
|
139
|
+
}
|
|
39
140
|
if (!isAuthoredComponent(filename))
|
|
40
141
|
return null;
|
|
41
142
|
const generated = compileComponentParts(source, filename, {
|
|
42
143
|
componentImports: 'source',
|
|
43
144
|
cssImport: `${filename}${styleSuffix}`,
|
|
145
|
+
hotState: development,
|
|
44
146
|
});
|
|
147
|
+
compiled.set(filename, generated);
|
|
148
|
+
const output = transpileComponent(generated.code, source, filename, generated.origins);
|
|
45
149
|
return {
|
|
46
|
-
code:
|
|
47
|
-
|
|
48
|
-
compilerOptions: {
|
|
49
|
-
module: ts.ModuleKind.ESNext,
|
|
50
|
-
target: ts.ScriptTarget.ES2022,
|
|
51
|
-
},
|
|
52
|
-
}).outputText,
|
|
53
|
-
map: null,
|
|
150
|
+
code: output.code,
|
|
151
|
+
map: JSON.stringify(output.map),
|
|
54
152
|
};
|
|
55
153
|
},
|
|
56
|
-
handleHotUpdate(context) {
|
|
154
|
+
async handleHotUpdate(context) {
|
|
155
|
+
if (options.foreign !== 'runtime' && isForeignSource(context.file)) {
|
|
156
|
+
const filename = context.file;
|
|
157
|
+
const source = await context.read();
|
|
158
|
+
const entry = options.foreign === 'automatic' && filename.endsWith('.tsx')
|
|
159
|
+
? convertReactRootEntry(source, filename)
|
|
160
|
+
: undefined;
|
|
161
|
+
if (!entry) {
|
|
162
|
+
const converted = convertForeignComponent(source, filename, {
|
|
163
|
+
resolveReactImport: (specifier) => resolveReactComponentImport(filename, specifier),
|
|
164
|
+
});
|
|
165
|
+
const next = compileComponentParts(converted, `${filename}.workstar`);
|
|
166
|
+
foreignStyles.set(filename, next.css);
|
|
167
|
+
}
|
|
168
|
+
const components = [
|
|
169
|
+
context.server.moduleGraph.getModuleById(foreignPrefix + filename),
|
|
170
|
+
options.foreign === 'automatic'
|
|
171
|
+
? context.server.moduleGraph.getModuleById(filename)
|
|
172
|
+
: undefined,
|
|
173
|
+
].filter((module) => module !== undefined);
|
|
174
|
+
const stylesheet = context.server.moduleGraph.getModuleById(foreignStylePrefix + filename + '.css');
|
|
175
|
+
components.forEach((module) => context.server.moduleGraph.invalidateModule(module));
|
|
176
|
+
if (stylesheet)
|
|
177
|
+
context.server.moduleGraph.invalidateModule(stylesheet);
|
|
178
|
+
return [...components, stylesheet].filter((module) => module !== undefined);
|
|
179
|
+
}
|
|
57
180
|
if (!isAuthoredComponent(context.file))
|
|
58
181
|
return;
|
|
182
|
+
const previous = compiled.get(context.file);
|
|
183
|
+
const next = compileComponentParts(await context.read(), context.file, {
|
|
184
|
+
componentImports: 'source',
|
|
185
|
+
cssImport: `${context.file}${styleSuffix}`,
|
|
186
|
+
hotState: development,
|
|
187
|
+
});
|
|
59
188
|
const stylesheet = context.server.moduleGraph.getModuleById(`${context.file}${styleSuffix}`);
|
|
60
189
|
if (!stylesheet)
|
|
61
190
|
return;
|
|
62
191
|
context.server.moduleGraph.invalidateModule(stylesheet);
|
|
63
|
-
|
|
192
|
+
if (previous &&
|
|
193
|
+
previous.code === next.code &&
|
|
194
|
+
previous.css !== next.css) {
|
|
195
|
+
compiled.set(context.file, next);
|
|
196
|
+
return [stylesheet];
|
|
197
|
+
}
|
|
198
|
+
const roots = rootComponents(context.modules);
|
|
199
|
+
roots.forEach((module) => context.server.moduleGraph.invalidateModule(module));
|
|
200
|
+
return [...roots, stylesheet];
|
|
64
201
|
},
|
|
65
202
|
};
|
|
66
203
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { parseFragment } from 'parse5';
|
|
2
|
+
import { pathExpression, reject } from './compat-rules.js';
|
|
3
|
+
import { convertVueScript } from './vue-script-compat.js';
|
|
4
|
+
function content(source, node, filename) {
|
|
5
|
+
const location = node.sourceCodeLocation;
|
|
6
|
+
if (!location?.startTag || !location.endTag)
|
|
7
|
+
reject(filename, 'unclosed ' + node.tagName);
|
|
8
|
+
return source.slice(location.startTag.endOffset, location.endTag.startOffset);
|
|
9
|
+
}
|
|
10
|
+
function templatePath(path, refs, stores, filename) {
|
|
11
|
+
if (!pathExpression.test(path))
|
|
12
|
+
reject(filename, 'expression ' + path);
|
|
13
|
+
const root = path.split('.', 1)[0];
|
|
14
|
+
const fields = stores.get(root);
|
|
15
|
+
if (fields) {
|
|
16
|
+
const field = path.slice(root.length + 1).split('.', 1)[0];
|
|
17
|
+
if (!fields.has(field))
|
|
18
|
+
reject(filename, 'store field ' + path);
|
|
19
|
+
return path;
|
|
20
|
+
}
|
|
21
|
+
if (!refs.has(root))
|
|
22
|
+
return path;
|
|
23
|
+
const suffix = path.slice(root.length);
|
|
24
|
+
if (suffix.startsWith('.value'))
|
|
25
|
+
reject(filename, 'explicit .value in a Vue template');
|
|
26
|
+
return root + '.value' + suffix;
|
|
27
|
+
}
|
|
28
|
+
/** Converts Vue SFC markup with typed defineProps and optional CSS. */
|
|
29
|
+
export function convertVueComponent(source, filename = 'Component.vue') {
|
|
30
|
+
const blockErrors = [];
|
|
31
|
+
const fragment = parseFragment(source, {
|
|
32
|
+
sourceCodeLocationInfo: true,
|
|
33
|
+
onParseError: (error) => blockErrors.push(error.code),
|
|
34
|
+
});
|
|
35
|
+
if (blockErrors.length)
|
|
36
|
+
reject(filename, 'invalid SFC markup: ' + blockErrors[0]);
|
|
37
|
+
const meaningful = fragment.childNodes.filter((node) => !('value' in node) || node.value.trim());
|
|
38
|
+
if (meaningful.some((node) => !('tagName' in node)))
|
|
39
|
+
reject(filename, 'content outside SFC blocks');
|
|
40
|
+
const blocks = meaningful;
|
|
41
|
+
if (blocks.some((node) => !['template', 'script', 'style'].includes(node.tagName)))
|
|
42
|
+
reject(filename, 'unknown SFC block');
|
|
43
|
+
const templates = blocks.filter((node) => node.tagName === 'template');
|
|
44
|
+
const scripts = blocks.filter((node) => node.tagName === 'script');
|
|
45
|
+
const styles = blocks.filter((node) => node.tagName === 'style');
|
|
46
|
+
if (templates.length !== 1 || scripts.length > 1 || styles.length > 1)
|
|
47
|
+
reject(filename, 'SFC block count');
|
|
48
|
+
if (templates[0].attrs.length)
|
|
49
|
+
reject(filename, 'template attributes');
|
|
50
|
+
let scriptContent = '';
|
|
51
|
+
let refs = new Set();
|
|
52
|
+
let stores = new Map();
|
|
53
|
+
if (scripts[0]) {
|
|
54
|
+
const attrs = new Map(scripts[0].attrs.map((attribute) => [attribute.name, attribute.value]));
|
|
55
|
+
if (attrs.size !== 2 || !attrs.has('setup') || attrs.get('lang') !== 'ts')
|
|
56
|
+
reject(filename, 'script setup attributes');
|
|
57
|
+
const converted = convertVueScript(content(source, scripts[0], filename), filename);
|
|
58
|
+
scriptContent = [converted.props, converted.setup]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join('\n');
|
|
61
|
+
refs = converted.refs;
|
|
62
|
+
stores = converted.stores;
|
|
63
|
+
}
|
|
64
|
+
let markup = content(source, templates[0], filename);
|
|
65
|
+
const templateErrors = [];
|
|
66
|
+
const templateTree = parseFragment(markup, {
|
|
67
|
+
sourceCodeLocationInfo: true,
|
|
68
|
+
onParseError: (error) => templateErrors.push(error.code),
|
|
69
|
+
});
|
|
70
|
+
if (templateErrors.length)
|
|
71
|
+
reject(filename, 'invalid template markup: ' + templateErrors[0]);
|
|
72
|
+
const replacements = [];
|
|
73
|
+
const visit = (nodes) => {
|
|
74
|
+
for (const node of nodes) {
|
|
75
|
+
if (!('tagName' in node)) {
|
|
76
|
+
if ('value' in node && node.sourceCodeLocation) {
|
|
77
|
+
const offset = node.sourceCodeLocation.startOffset;
|
|
78
|
+
const raw = markup.slice(offset, node.sourceCodeLocation.endOffset);
|
|
79
|
+
for (const match of raw.matchAll(/\{\{([^{}]+)\}\}/g)) {
|
|
80
|
+
const value = templatePath(match[1].trim(), refs, stores, filename);
|
|
81
|
+
replacements.push({
|
|
82
|
+
start: offset + match.index,
|
|
83
|
+
end: offset + match.index + match[0].length,
|
|
84
|
+
value: '{' + value + '}',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const start = node.sourceCodeLocation?.startTag?.startOffset;
|
|
91
|
+
const originalTag = start === undefined
|
|
92
|
+
? ''
|
|
93
|
+
: /^<\s*([A-Za-z][\w-]*)/.exec(markup.slice(start))?.[1];
|
|
94
|
+
if (!originalTag ||
|
|
95
|
+
/^[A-Z]/.test(originalTag) ||
|
|
96
|
+
originalTag === 'slot' ||
|
|
97
|
+
originalTag === 'component')
|
|
98
|
+
reject(filename, 'Vue component tag');
|
|
99
|
+
for (const attribute of node.attrs) {
|
|
100
|
+
const name = attribute.name;
|
|
101
|
+
if (!name.startsWith(':') && !name.startsWith('@')) {
|
|
102
|
+
if (name.startsWith('v-') ||
|
|
103
|
+
name.startsWith('#') ||
|
|
104
|
+
name === 'ref' ||
|
|
105
|
+
name === 'key')
|
|
106
|
+
reject(filename, 'Vue directive');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const target = name.slice(1);
|
|
110
|
+
if (!/^[a-z][a-z0-9-]*$/.test(target) ||
|
|
111
|
+
target === 'style' ||
|
|
112
|
+
target === 'class' ||
|
|
113
|
+
target === 'key' ||
|
|
114
|
+
target === 'ref' ||
|
|
115
|
+
!pathExpression.test(attribute.value)) {
|
|
116
|
+
reject(filename, 'Vue directive ' + name);
|
|
117
|
+
}
|
|
118
|
+
const root = attribute.value.split('.', 1)[0];
|
|
119
|
+
if (name.startsWith('@') && (refs.has(root) || stores.has(root)))
|
|
120
|
+
reject(filename, 'state used as event handler');
|
|
121
|
+
const location = node.sourceCodeLocation?.attrs?.[name];
|
|
122
|
+
if (!location)
|
|
123
|
+
reject(filename, 'Vue directive ' + name);
|
|
124
|
+
replacements.push({
|
|
125
|
+
start: location.startOffset,
|
|
126
|
+
end: location.endOffset,
|
|
127
|
+
value: (name.startsWith('@') ? 'on:' : '') +
|
|
128
|
+
target +
|
|
129
|
+
'={' +
|
|
130
|
+
(name.startsWith('@')
|
|
131
|
+
? attribute.value
|
|
132
|
+
: templatePath(attribute.value, refs, stores, filename)) +
|
|
133
|
+
'}',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
visit(node.tagName === 'template'
|
|
137
|
+
? node.content.childNodes
|
|
138
|
+
: node.childNodes);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
visit(templateTree.childNodes);
|
|
142
|
+
for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
|
|
143
|
+
markup =
|
|
144
|
+
markup.slice(0, replacement.start) +
|
|
145
|
+
replacement.value +
|
|
146
|
+
markup.slice(replacement.end);
|
|
147
|
+
}
|
|
148
|
+
if (markup.includes('{{') || markup.includes('}}'))
|
|
149
|
+
reject(filename, 'unparsed interpolation');
|
|
150
|
+
let style = '';
|
|
151
|
+
if (styles[0]) {
|
|
152
|
+
const attrs = styles[0].attrs;
|
|
153
|
+
if (attrs.length > 1 || (attrs.length === 1 && attrs[0]?.name !== 'scoped'))
|
|
154
|
+
reject(filename, 'style attributes');
|
|
155
|
+
style =
|
|
156
|
+
'<style' +
|
|
157
|
+
(attrs.length ? '' : ' global') +
|
|
158
|
+
'>\n' +
|
|
159
|
+
content(source, styles[0], filename) +
|
|
160
|
+
'\n</style>\n';
|
|
161
|
+
}
|
|
162
|
+
return ((scriptContent
|
|
163
|
+
? '<script lang="ts">\n' + scriptContent + '\n</script>\n'
|
|
164
|
+
: '') +
|
|
165
|
+
markup +
|
|
166
|
+
'\n' +
|
|
167
|
+
style);
|
|
168
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface VueScriptCompat {
|
|
2
|
+
readonly props: string;
|
|
3
|
+
readonly setup: string;
|
|
4
|
+
readonly refs: ReadonlySet<string>;
|
|
5
|
+
readonly stores: ReadonlyMap<string, ReadonlySet<string>>;
|
|
6
|
+
}
|
|
7
|
+
/** Translate primitive Vue refs, flat reactive records, props, and their handlers. */
|
|
8
|
+
export declare function convertVueScript(script: string, filename: string): VueScriptCompat;
|