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/README.md
CHANGED
|
@@ -10,10 +10,21 @@ The `workstar-compile` command compiles one component or a directory:
|
|
|
10
10
|
workstar-compile --all src .workstar/generated --css .workstar/styles.css
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
For Vite, import `workstar` from `workstar-compiler/vite` and add `workstar()` to `plugins`. Vite compiles
|
|
13
|
+
For Vite, import `workstar` from `workstar-compiler/vite` and add `workstar()` to `plugins`. Vite compiles explicit `?workstar` component imports in memory. Use `workstar({ foreign: 'automatic' })` to compile ordinary TSX/Vue imports under `src`, including a supported `createRoot(...).render(...)` entry. Use `workstar({ foreign: 'runtime' })` for unchanged React TSX applications that need Workstar-backed state and routing. The runtime mode is experimental and does not yet reproduce every React behavior. The CLI is for explicit output and type checking.
|
|
14
|
+
|
|
15
|
+
During Vite development, the compiler adds state identities for direct local
|
|
16
|
+
`signal()` declarations. Pass a stable `HotContext` from `workstar/dev` to a
|
|
17
|
+
root component when accepting Vite updates, as shown in the static starter.
|
|
18
|
+
Production output does not include this instrumentation. The compiler maps
|
|
19
|
+
script statements and the markup entry point back to authored source in Vite;
|
|
20
|
+
individual markup expressions are not mapped yet.
|
|
14
21
|
|
|
15
22
|
Inside `<script lang="ts">`, imports and an optional exported `Props` type define the component contract. Top-level `const`, `let`, and function declarations create state and behavior for each component instance; import `signal` from `workstar` for reactive local state. Keep module-wide shared state in an imported TypeScript module when sharing is intentional.
|
|
16
23
|
|
|
24
|
+
Use `bind:attrs={record}` on a native element to spread a plain record of checked HTML attributes. The record can be reactive. Event handlers, styles, refs, and unsafe URLs are rejected; bind events explicitly with `on:event`.
|
|
25
|
+
|
|
17
26
|
`<noscript>` accepts static text only. Put links and localized expressions elsewhere in the page; nested markup would be parsed as raw text and dynamic markers cannot hydrate reliably.
|
|
18
27
|
|
|
19
28
|
See the [Workstar repository](https://github.com/wslab-ai/workstar) for starters and current limitations.
|
|
29
|
+
|
|
30
|
+
For experimental React-style TSX and Vue SFC source conversion without their runtimes, see the [compatibility guide](https://github.com/wslab-ai/workstar/blob/main/docs/foreign-components.md). Use `?workstar` imports with the Vite plugin or `workstar-compile --compat` with any other build system.
|
package/bin/workstar-compile.mjs
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
compileViewFile,
|
|
5
|
+
compileForeignFile,
|
|
5
6
|
compileViewDirectory,
|
|
6
7
|
watchViewDirectory,
|
|
8
|
+
auditForeignDirectory,
|
|
7
9
|
} from '../dist/src/project.js';
|
|
8
10
|
|
|
9
11
|
const args = process.argv.slice(2);
|
|
@@ -13,7 +15,10 @@ try {
|
|
|
13
15
|
(args.length === 4 && args[2] === '--css')
|
|
14
16
|
? { cssOutputPath: resolve(args.at(-1)) }
|
|
15
17
|
: {};
|
|
16
|
-
if (
|
|
18
|
+
if (args.length === 2 && args[0] === '--compat-audit') {
|
|
19
|
+
const report = await auditForeignDirectory(resolve(args[1]));
|
|
20
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
21
|
+
} else if (
|
|
17
22
|
(args.length === 3 || (args.length === 5 && args[3] === '--css')) &&
|
|
18
23
|
args[0] === '--all'
|
|
19
24
|
) {
|
|
@@ -35,6 +40,13 @@ try {
|
|
|
35
40
|
process.stdout.write(
|
|
36
41
|
`Watching ${resolve(args[1])} for .workstar changes.\n`,
|
|
37
42
|
);
|
|
43
|
+
} else if (
|
|
44
|
+
(args.length === 3 || (args.length === 5 && args[3] === '--css')) &&
|
|
45
|
+
args[0] === '--compat' &&
|
|
46
|
+
/\.(tsx|vue)$/.test(args[1] ?? '') &&
|
|
47
|
+
args[2]?.endsWith('.ts')
|
|
48
|
+
) {
|
|
49
|
+
await compileForeignFile(resolve(args[1]), resolve(args[2]), cssOption);
|
|
38
50
|
} else if (
|
|
39
51
|
(args.length === 2 || (args.length === 4 && args[2] === '--css')) &&
|
|
40
52
|
args[0]?.endsWith('.workstar') &&
|
|
@@ -45,7 +57,9 @@ try {
|
|
|
45
57
|
} else {
|
|
46
58
|
process.stderr.write(
|
|
47
59
|
'Usage: workstar-compile input.workstar output.ts [--css public/components.css]\n' +
|
|
60
|
+
' workstar-compile --compat input.tsx|input.vue output.ts [--css public/components.css]\n' +
|
|
48
61
|
' workstar-compile --all source-directory output-directory [--css public/components.css]\n' +
|
|
62
|
+
' workstar-compile --compat-audit source-directory\n' +
|
|
49
63
|
' workstar-compile --watch source-directory output-directory [--css public/components.css]\n',
|
|
50
64
|
);
|
|
51
65
|
process.exitCode = 2;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { convertReactComponent } from './react-compat.js';
|
|
2
|
+
export { convertVueComponent } from './vue-compat.js';
|
|
3
|
+
export declare function convertForeignComponent(source: string, filename: string, options?: {
|
|
4
|
+
resolveReactImport?: (specifier: string) => string | undefined;
|
|
5
|
+
}): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { convertReactComponent } from './react-compat.js';
|
|
2
|
+
import { convertVueComponent } from './vue-compat.js';
|
|
3
|
+
import { reject } from './compat-rules.js';
|
|
4
|
+
export { convertReactComponent } from './react-compat.js';
|
|
5
|
+
export { convertVueComponent } from './vue-compat.js';
|
|
6
|
+
export function convertForeignComponent(source, filename, options = {}) {
|
|
7
|
+
if (filename.endsWith('.tsx'))
|
|
8
|
+
return convertReactComponent(source, filename, options);
|
|
9
|
+
if (filename.endsWith('.vue'))
|
|
10
|
+
return convertVueComponent(source, filename);
|
|
11
|
+
return reject(filename, 'file extension');
|
|
12
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { DefaultTreeAdapterTypes as Html } from 'parse5';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export interface ScriptStatement {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly sourceOffset?: number | undefined;
|
|
5
|
+
readonly exact?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function componentScript(node: Html.Element, filename: string, componentImports: 'generated' | 'source', sourcePosition: (offset: number) => {
|
|
8
|
+
line: number;
|
|
9
|
+
column: number;
|
|
10
|
+
}, rewriteRelativeImport?: (specifier: string) => string, hotState?: boolean): {
|
|
11
|
+
moduleStatements: ScriptStatement[];
|
|
12
|
+
setupStatements: ScriptStatement[];
|
|
5
13
|
props: string[];
|
|
6
14
|
};
|
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { ComponentCompileError } from './errors.js';
|
|
3
|
+
function preserveLocalSignals(statement, file, signalBindings) {
|
|
4
|
+
const statementStart = statement.getStart(file);
|
|
5
|
+
let code = statement.getText(file);
|
|
6
|
+
const signals = statement.declarationList.declarations.flatMap((declaration) => {
|
|
7
|
+
const initializer = declaration.initializer;
|
|
8
|
+
if (!ts.isIdentifier(declaration.name) ||
|
|
9
|
+
!initializer ||
|
|
10
|
+
!ts.isCallExpression(initializer) ||
|
|
11
|
+
!ts.isIdentifier(initializer.expression) ||
|
|
12
|
+
!signalBindings.has(initializer.expression.text))
|
|
13
|
+
return [];
|
|
14
|
+
return [{ name: declaration.name.text, initializer }];
|
|
15
|
+
});
|
|
16
|
+
for (const { name, initializer } of signals.reverse()) {
|
|
17
|
+
const start = initializer.getStart(file) - statementStart;
|
|
18
|
+
const end = initializer.getEnd() - statementStart;
|
|
19
|
+
const original = code.slice(start, end);
|
|
20
|
+
const retained = `__context?.state(${JSON.stringify(`${name}:${original}`)}, () => ${original})`;
|
|
21
|
+
code =
|
|
22
|
+
code.slice(0, start) + `(${retained} ?? ${original})` + code.slice(end);
|
|
23
|
+
}
|
|
24
|
+
return signals.length > 0 ? code : undefined;
|
|
25
|
+
}
|
|
26
|
+
export function componentScript(node, filename, componentImports, sourcePosition, rewriteRelativeImport, hotState = false) {
|
|
4
27
|
if (node.attrs.length !== 1 ||
|
|
5
28
|
node.attrs[0]?.name !== 'lang' ||
|
|
6
29
|
node.attrs[0].value !== 'ts') {
|
|
7
|
-
|
|
30
|
+
throw new ComponentCompileError('The component script must be <script lang="ts">.', filename, node.sourceCodeLocation
|
|
31
|
+
? sourcePosition(node.sourceCodeLocation.startOffset)
|
|
32
|
+
: undefined);
|
|
8
33
|
}
|
|
34
|
+
const scriptStart = node.sourceCodeLocation?.startTag?.endOffset;
|
|
35
|
+
const failAt = (message, offset = 0) => {
|
|
36
|
+
throw new ComponentCompileError(message, filename, scriptStart === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: sourcePosition(scriptStart + offset));
|
|
39
|
+
};
|
|
9
40
|
const script = node.childNodes
|
|
10
41
|
.map((child) => {
|
|
11
42
|
if (!('value' in child))
|
|
12
|
-
|
|
43
|
+
return failAt('Invalid script content.');
|
|
13
44
|
return child.value;
|
|
14
45
|
})
|
|
15
46
|
.join('');
|
|
@@ -18,90 +49,128 @@ export function componentScript(node, filename, componentImports, rewriteRelativ
|
|
|
18
49
|
fileName: filename.replace(/\.workstar$/, '.ts'),
|
|
19
50
|
reportDiagnostics: true,
|
|
20
51
|
});
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
const syntaxError = syntax.diagnostics?.find((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
|
53
|
+
if (syntaxError) {
|
|
54
|
+
failAt(`Invalid TypeScript in component script: ${ts.flattenDiagnosticMessageText(syntaxError.messageText, ' ')}`, syntaxError.start ?? 0);
|
|
23
55
|
}
|
|
24
56
|
let props;
|
|
57
|
+
const signalBindings = new Set();
|
|
25
58
|
const moduleStatements = [];
|
|
26
59
|
const setupStatements = [];
|
|
27
60
|
for (const statement of file.statements) {
|
|
28
61
|
if (ts.isEmptyStatement(statement))
|
|
29
62
|
continue;
|
|
63
|
+
const sourceOffset = scriptStart === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: scriptStart + statement.getStart(file);
|
|
30
66
|
if (ts.isImportDeclaration(statement)) {
|
|
31
67
|
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
|
|
32
68
|
? statement.moduleSpecifier.text
|
|
33
69
|
: '';
|
|
70
|
+
const named = statement.importClause?.namedBindings;
|
|
71
|
+
if (specifier === 'workstar' && named && ts.isNamedImports(named)) {
|
|
72
|
+
for (const binding of named.elements) {
|
|
73
|
+
if ((binding.propertyName ?? binding.name).text === 'signal') {
|
|
74
|
+
signalBindings.add(binding.name.text);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
34
78
|
if (specifier.endsWith('.workstar')) {
|
|
35
79
|
const binding = statement.importClause?.name?.text;
|
|
36
80
|
if (!/^(?:\.{1,2}\/)+(?:[A-Za-z0-9][\w-]*\/)*[A-Za-z0-9][\w-]*\.workstar$/.test(specifier) ||
|
|
37
81
|
!binding ||
|
|
38
82
|
statement.importClause?.isTypeOnly ||
|
|
39
83
|
statement.importClause?.namedBindings) {
|
|
40
|
-
|
|
84
|
+
failAt('Import a relative .workstar view with a default import.', statement.getStart(file));
|
|
41
85
|
}
|
|
42
|
-
moduleStatements.push(
|
|
86
|
+
moduleStatements.push({
|
|
87
|
+
code: `import { render as ${binding} } from '${componentImports === 'source' ? specifier : specifier.slice(0, -'.workstar'.length)}';`,
|
|
88
|
+
sourceOffset,
|
|
89
|
+
});
|
|
43
90
|
}
|
|
44
91
|
else if (specifier.startsWith('.') && rewriteRelativeImport) {
|
|
45
92
|
const importText = statement.getText(file);
|
|
46
93
|
const start = statement.moduleSpecifier.getStart(file) - statement.getStart(file);
|
|
47
94
|
const end = statement.moduleSpecifier.getEnd() - statement.getStart(file);
|
|
48
|
-
moduleStatements.push(
|
|
49
|
-
|
|
50
|
-
|
|
95
|
+
moduleStatements.push({
|
|
96
|
+
code: importText.slice(0, start) +
|
|
97
|
+
JSON.stringify(rewriteRelativeImport(specifier)) +
|
|
98
|
+
importText.slice(end),
|
|
99
|
+
sourceOffset,
|
|
100
|
+
});
|
|
51
101
|
}
|
|
52
102
|
else {
|
|
53
|
-
moduleStatements.push(
|
|
103
|
+
moduleStatements.push({
|
|
104
|
+
code: statement.getText(file),
|
|
105
|
+
sourceOffset,
|
|
106
|
+
exact: true,
|
|
107
|
+
});
|
|
54
108
|
}
|
|
55
109
|
continue;
|
|
56
110
|
}
|
|
57
111
|
if (ts.isInterfaceDeclaration(statement) &&
|
|
58
112
|
statement.name.text === 'Props') {
|
|
59
113
|
if (props)
|
|
60
|
-
|
|
114
|
+
failAt('The component can declare Props only once.', statement.getStart(file));
|
|
61
115
|
props = statement.members.map((member) => {
|
|
62
116
|
if (!ts.isPropertySignature(member) ||
|
|
63
117
|
!member.name ||
|
|
64
118
|
!ts.isIdentifier(member.name)) {
|
|
65
|
-
|
|
119
|
+
return failAt('Props must use named properties.', member.getStart(file));
|
|
66
120
|
}
|
|
67
121
|
return member.name.text;
|
|
68
122
|
});
|
|
69
|
-
moduleStatements.push(
|
|
123
|
+
moduleStatements.push({
|
|
124
|
+
code: statement.getText(file),
|
|
125
|
+
sourceOffset,
|
|
126
|
+
exact: true,
|
|
127
|
+
});
|
|
70
128
|
continue;
|
|
71
129
|
}
|
|
72
130
|
if (ts.isTypeAliasDeclaration(statement) &&
|
|
73
131
|
statement.name.text === 'Props' &&
|
|
74
132
|
ts.isTypeLiteralNode(statement.type)) {
|
|
75
133
|
if (props)
|
|
76
|
-
|
|
134
|
+
failAt('The component can declare Props only once.', statement.getStart(file));
|
|
77
135
|
props = statement.type.members.map((member) => {
|
|
78
136
|
if (!ts.isPropertySignature(member) ||
|
|
79
137
|
!member.name ||
|
|
80
138
|
!ts.isIdentifier(member.name)) {
|
|
81
|
-
|
|
139
|
+
return failAt('Props must use named properties.', member.getStart(file));
|
|
82
140
|
}
|
|
83
141
|
return member.name.text;
|
|
84
142
|
});
|
|
85
|
-
moduleStatements.push(
|
|
143
|
+
moduleStatements.push({
|
|
144
|
+
code: statement.getText(file),
|
|
145
|
+
sourceOffset,
|
|
146
|
+
exact: true,
|
|
147
|
+
});
|
|
86
148
|
continue;
|
|
87
149
|
}
|
|
88
150
|
if (ts.isVariableStatement(statement) ||
|
|
89
151
|
ts.isFunctionDeclaration(statement)) {
|
|
90
152
|
if (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
91
|
-
|
|
153
|
+
failAt('Component-local declarations cannot be exported.', statement.getStart(file));
|
|
92
154
|
}
|
|
93
|
-
|
|
155
|
+
const retained = hotState && ts.isVariableStatement(statement)
|
|
156
|
+
? preserveLocalSignals(statement, file, signalBindings)
|
|
157
|
+
: undefined;
|
|
158
|
+
setupStatements.push({
|
|
159
|
+
code: retained ?? statement.getText(file),
|
|
160
|
+
sourceOffset,
|
|
161
|
+
exact: retained === undefined,
|
|
162
|
+
});
|
|
94
163
|
continue;
|
|
95
164
|
}
|
|
96
|
-
|
|
165
|
+
failAt('Only imports, Props, and component-local variables/functions are supported in the script.', statement.getStart(file));
|
|
97
166
|
}
|
|
98
167
|
if (!props) {
|
|
99
168
|
props = [];
|
|
100
|
-
moduleStatements.push('type Props = Record<string, never>;');
|
|
169
|
+
moduleStatements.push({ code: 'type Props = Record<string, never>;' });
|
|
101
170
|
}
|
|
102
171
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
172
|
+
moduleStatements,
|
|
173
|
+
setupStatements,
|
|
105
174
|
props,
|
|
106
175
|
};
|
|
107
176
|
}
|
|
@@ -2,6 +2,11 @@ export declare const controlAttribute = "data-workstar-compiler-control";
|
|
|
2
2
|
export interface NormalizedControls {
|
|
3
3
|
source: string;
|
|
4
4
|
count: number;
|
|
5
|
+
originalOffset: (normalizedOffset: number) => number;
|
|
6
|
+
}
|
|
7
|
+
export declare class ControlSyntaxError extends Error {
|
|
8
|
+
readonly offset: number;
|
|
9
|
+
constructor(message: string, offset: number);
|
|
5
10
|
}
|
|
6
11
|
/** Adapt authoring controls to HTML parser insertion modes, including select and table. */
|
|
7
12
|
export declare function normalizeControls(source: string): NormalizedControls;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const controlNames = new Set(['Each', 'If', 'Else', 'Use']);
|
|
2
2
|
export const controlAttribute = 'data-workstar-compiler-control';
|
|
3
|
+
export class ControlSyntaxError extends Error {
|
|
4
|
+
offset;
|
|
5
|
+
constructor(message, offset) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.offset = offset;
|
|
8
|
+
this.name = 'ControlSyntaxError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
function tagEnd(source, start) {
|
|
4
12
|
let quote;
|
|
5
13
|
for (let index = start + 1; index < source.length; index++) {
|
|
@@ -15,19 +23,28 @@ function tagEnd(source, start) {
|
|
|
15
23
|
return index;
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
|
-
throw new
|
|
26
|
+
throw new ControlSyntaxError('Unclosed HTML tag.', start);
|
|
19
27
|
}
|
|
20
28
|
/** Adapt authoring controls to HTML parser insertion modes, including select and table. */
|
|
21
29
|
export function normalizeControls(source) {
|
|
22
30
|
if (source.includes(controlAttribute)) {
|
|
23
|
-
throw new
|
|
31
|
+
throw new ControlSyntaxError(`${controlAttribute} is reserved for the compiler.`, source.indexOf(controlAttribute));
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let result = source.slice(0, start);
|
|
28
|
-
let cursor = start;
|
|
33
|
+
let result = '';
|
|
34
|
+
let cursor = 0;
|
|
29
35
|
let count = 0;
|
|
30
36
|
const stack = [];
|
|
37
|
+
const replacements = [];
|
|
38
|
+
const appendReplacement = (authoredStart, authoredEnd, replacement) => {
|
|
39
|
+
const generatedStart = result.length;
|
|
40
|
+
result += replacement;
|
|
41
|
+
replacements.push({
|
|
42
|
+
generatedStart,
|
|
43
|
+
generatedEnd: result.length,
|
|
44
|
+
authoredStart,
|
|
45
|
+
authoredEnd,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
31
48
|
while (cursor < source.length) {
|
|
32
49
|
const opening = source.indexOf('<', cursor);
|
|
33
50
|
if (opening < 0)
|
|
@@ -36,7 +53,7 @@ export function normalizeControls(source) {
|
|
|
36
53
|
if (source.startsWith('<!--', opening)) {
|
|
37
54
|
const end = source.indexOf('-->', opening + 4);
|
|
38
55
|
if (end < 0)
|
|
39
|
-
throw new
|
|
56
|
+
throw new ControlSyntaxError('Unclosed HTML comment.', opening);
|
|
40
57
|
result += source.slice(opening, end + 3);
|
|
41
58
|
cursor = end + 3;
|
|
42
59
|
continue;
|
|
@@ -48,8 +65,9 @@ export function normalizeControls(source) {
|
|
|
48
65
|
if (!name || !controlNames.has(name)) {
|
|
49
66
|
result += tag;
|
|
50
67
|
cursor = end + 1;
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
const rawTextTag = /^<(script|style|textarea)(?=[\s>])/i.exec(tag)?.[1];
|
|
69
|
+
if (rawTextTag) {
|
|
70
|
+
const closing = new RegExp(`</${rawTextTag}\\s*>`, 'gi');
|
|
53
71
|
closing.lastIndex = cursor;
|
|
54
72
|
const close = closing.exec(source);
|
|
55
73
|
if (close) {
|
|
@@ -62,31 +80,48 @@ export function normalizeControls(source) {
|
|
|
62
80
|
const closing = match[1] === '/';
|
|
63
81
|
const selfClosing = /\/\s*>$/.test(tag);
|
|
64
82
|
if (closing) {
|
|
65
|
-
if (stack.pop() !== name) {
|
|
66
|
-
throw new
|
|
83
|
+
if (stack.pop()?.name !== name) {
|
|
84
|
+
throw new ControlSyntaxError(`Mismatched </${name}> control element.`, opening);
|
|
67
85
|
}
|
|
68
|
-
|
|
86
|
+
appendReplacement(opening, end + 1, '</template>');
|
|
69
87
|
}
|
|
70
88
|
else {
|
|
71
89
|
if (selfClosing && name !== 'Use') {
|
|
72
|
-
throw new
|
|
90
|
+
throw new ControlSyntaxError(`<${name}> cannot be self-closing.`, opening);
|
|
73
91
|
}
|
|
74
92
|
const authoredAttributes = tag.slice(match[0].length, -1);
|
|
75
93
|
const attributes = selfClosing
|
|
76
94
|
? authoredAttributes.replace(/\/\s*$/, '')
|
|
77
95
|
: authoredAttributes;
|
|
78
|
-
|
|
79
|
-
if (selfClosing)
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
stack.push(name);
|
|
96
|
+
appendReplacement(opening, end + 1, `<template ${controlAttribute}="${name}"${attributes}>${selfClosing ? '</template>' : ''}`);
|
|
97
|
+
if (!selfClosing)
|
|
98
|
+
stack.push({ name, offset: opening });
|
|
83
99
|
count++;
|
|
84
100
|
}
|
|
85
101
|
cursor = end + 1;
|
|
86
102
|
}
|
|
87
103
|
result += source.slice(cursor);
|
|
88
104
|
if (stack.length > 0) {
|
|
89
|
-
|
|
105
|
+
const unclosed = stack.at(-1);
|
|
106
|
+
throw new ControlSyntaxError(`Unclosed <${unclosed.name}> control element.`, unclosed.offset);
|
|
90
107
|
}
|
|
91
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
source: result,
|
|
110
|
+
count,
|
|
111
|
+
originalOffset(normalizedOffset) {
|
|
112
|
+
let difference = 0;
|
|
113
|
+
for (const replacement of replacements) {
|
|
114
|
+
if (normalizedOffset < replacement.generatedStart)
|
|
115
|
+
break;
|
|
116
|
+
if (normalizedOffset < replacement.generatedEnd) {
|
|
117
|
+
return replacement.authoredStart;
|
|
118
|
+
}
|
|
119
|
+
difference +=
|
|
120
|
+
replacement.generatedEnd -
|
|
121
|
+
replacement.generatedStart -
|
|
122
|
+
(replacement.authoredEnd - replacement.authoredStart);
|
|
123
|
+
}
|
|
124
|
+
return normalizedOffset - difference;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
92
127
|
}
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export declare class ComponentCompileError extends Error {
|
|
2
|
+
readonly description: string;
|
|
2
3
|
readonly filename: string;
|
|
3
|
-
|
|
4
|
+
readonly position?: {
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly column: number;
|
|
7
|
+
} | undefined;
|
|
8
|
+
constructor(description: string, filename: string, position?: {
|
|
9
|
+
readonly line: number;
|
|
10
|
+
readonly column: number;
|
|
11
|
+
} | undefined);
|
|
4
12
|
}
|
|
5
13
|
export declare function fail(filename: string, message: string): never;
|
package/dist/src/errors.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export class ComponentCompileError extends Error {
|
|
2
|
+
description;
|
|
2
3
|
filename;
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
position;
|
|
5
|
+
constructor(description, filename, position) {
|
|
6
|
+
super(`${filename}${position ? `:${position.line}:${position.column}` : ''}: ${description}`);
|
|
7
|
+
this.description = description;
|
|
5
8
|
this.filename = filename;
|
|
9
|
+
this.position = position;
|
|
6
10
|
this.name = 'ComponentCompileError';
|
|
7
11
|
}
|
|
8
12
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import { type SourceOrigin } from './source-origin.js';
|
|
1
2
|
export { ComponentCompileError } from './errors.js';
|
|
2
3
|
/** Compile a typed component and its optional co-located stylesheet. */
|
|
3
4
|
export declare function compileComponentParts(source: string, filename?: string, options?: {
|
|
4
5
|
componentImports?: 'generated' | 'source';
|
|
5
6
|
rewriteRelativeImport?: (specifier: string) => string;
|
|
6
7
|
cssImport?: string;
|
|
8
|
+
hotState?: boolean;
|
|
7
9
|
}): {
|
|
8
10
|
code: string;
|
|
9
11
|
css: string;
|
|
12
|
+
origins: SourceOrigin[];
|
|
10
13
|
};
|
|
11
14
|
/** Compile a component to a TypeScript module; use parts to emit its CSS. */
|
|
12
15
|
export declare function compileComponent(source: string, filename?: string, options?: {
|
|
13
16
|
componentImports?: 'generated' | 'source';
|
|
14
17
|
rewriteRelativeImport?: (specifier: string) => string;
|
|
15
18
|
cssImport?: string;
|
|
19
|
+
hotState?: boolean;
|
|
16
20
|
}): string;
|