workstar-compiler 0.2.4-beta.0 → 0.2.4-beta.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.
- package/README.md +2 -0
- package/dist/src/project.d.ts +8 -0
- package/dist/src/project.js +48 -2
- package/dist/src/runtime-diagnostics.d.ts +2 -0
- package/dist/src/runtime-diagnostics.js +108 -0
- package/dist/src/vite.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ Inside `<script lang="ts">`, imports and an optional exported `Props` type defin
|
|
|
23
23
|
|
|
24
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
25
|
|
|
26
|
+
Run `workstar-compile --compat-audit src` before migrating a TSX or Vue tree. The JSON report identifies the component and first unsupported construct in each file, suggests runtime mode when appropriate, and inventories packages handled by Workstar aliases or requiring browser verification. Runtime-mode Vite builds fail early on unsupported React API imports and include the source position and implemented alternatives.
|
|
27
|
+
|
|
26
28
|
`<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.
|
|
27
29
|
|
|
28
30
|
See the [Workstar repository](https://github.com/wslab-ai/workstar) for starters and current limitations.
|
package/dist/src/project.d.ts
CHANGED
|
@@ -3,13 +3,21 @@ export interface ProjectStyles {
|
|
|
3
3
|
}
|
|
4
4
|
export interface ForeignAuditEntry {
|
|
5
5
|
filename: string;
|
|
6
|
+
component: string;
|
|
6
7
|
supported: boolean;
|
|
7
8
|
reason?: string;
|
|
9
|
+
suggestion?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ForeignDependency {
|
|
12
|
+
package: string;
|
|
13
|
+
files: string[];
|
|
14
|
+
handling: 'workstar-runtime-alias' | 'requires-browser-verification';
|
|
8
15
|
}
|
|
9
16
|
export interface ForeignAudit {
|
|
10
17
|
total: number;
|
|
11
18
|
supported: number;
|
|
12
19
|
entries: ForeignAuditEntry[];
|
|
20
|
+
dependencies: ForeignDependency[];
|
|
13
21
|
}
|
|
14
22
|
/** Compile one authored Workstar view without rewriting unrelated generated modules. */
|
|
15
23
|
export declare function compileViewFile(sourcePath: string, outputPath: string, options?: ProjectStyles): Promise<void>;
|
package/dist/src/project.js
CHANGED
|
@@ -5,6 +5,33 @@ import { compileComponentParts } from './index.js';
|
|
|
5
5
|
import { convertForeignComponent } from './compat.js';
|
|
6
6
|
import { reactComponentExportName } from './react-compat.js';
|
|
7
7
|
import { resolveReactComponentImport } from './react-import-resolution.js';
|
|
8
|
+
function componentName(source, filename) {
|
|
9
|
+
const match = /export\s+default\s+function\s+([A-Za-z_$][\w$]*)/.exec(source) ??
|
|
10
|
+
/export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/.exec(source) ??
|
|
11
|
+
/export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/.exec(source);
|
|
12
|
+
return (match?.[1] ??
|
|
13
|
+
filename
|
|
14
|
+
.split(/[\\/]/)
|
|
15
|
+
.at(-1)
|
|
16
|
+
.replace(/\.[^.]+$/, ''));
|
|
17
|
+
}
|
|
18
|
+
function packageName(specifier) {
|
|
19
|
+
if (specifier.startsWith('.') ||
|
|
20
|
+
specifier.startsWith('/') ||
|
|
21
|
+
specifier.startsWith('#'))
|
|
22
|
+
return undefined;
|
|
23
|
+
const parts = specifier.split('/');
|
|
24
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
25
|
+
}
|
|
26
|
+
function sourcePackages(source) {
|
|
27
|
+
const packages = new Set();
|
|
28
|
+
for (const match of source.matchAll(/(?:from\s+|import\s*\(|require\s*\()\s*['"]([^'"]+)['"]/g)) {
|
|
29
|
+
const name = packageName(match[1] ?? '');
|
|
30
|
+
if (name)
|
|
31
|
+
packages.add(name);
|
|
32
|
+
}
|
|
33
|
+
return [...packages];
|
|
34
|
+
}
|
|
8
35
|
async function filesInDirectory(directory, include, prefix = '') {
|
|
9
36
|
const entries = await readdir(join(directory, prefix), {
|
|
10
37
|
withFileTypes: true,
|
|
@@ -66,21 +93,31 @@ export async function compileForeignFile(sourcePath, outputPath, options = {}) {
|
|
|
66
93
|
export async function auditForeignDirectory(sourceDirectory) {
|
|
67
94
|
const paths = await filesInDirectory(sourceDirectory, (name) => /\.(tsx|vue)$/.test(name) &&
|
|
68
95
|
!/\.(test|spec|stories)\.(tsx|vue)$/.test(name));
|
|
96
|
+
const dependencyFiles = new Map();
|
|
69
97
|
const entries = await Promise.all(paths.map(async (filename) => {
|
|
70
98
|
const sourcePath = join(sourceDirectory, filename);
|
|
99
|
+
const source = await readFile(sourcePath, 'utf8');
|
|
100
|
+
for (const dependency of sourcePackages(source)) {
|
|
101
|
+
const files = dependencyFiles.get(dependency) ?? new Set();
|
|
102
|
+
files.add(filename);
|
|
103
|
+
dependencyFiles.set(dependency, files);
|
|
104
|
+
}
|
|
105
|
+
const component = componentName(source, filename);
|
|
71
106
|
try {
|
|
72
|
-
const converted = convertForeignComponent(
|
|
107
|
+
const converted = convertForeignComponent(source, sourcePath, {
|
|
73
108
|
resolveReactImport: (specifier) => resolveReactComponentImport(sourcePath, specifier),
|
|
74
109
|
});
|
|
75
110
|
compileComponentParts(converted, `${sourcePath}.workstar`);
|
|
76
|
-
return { filename, supported: true };
|
|
111
|
+
return { filename, component, supported: true };
|
|
77
112
|
}
|
|
78
113
|
catch (error) {
|
|
79
114
|
const message = error instanceof Error ? error.message : String(error);
|
|
80
115
|
return {
|
|
81
116
|
filename,
|
|
117
|
+
component,
|
|
82
118
|
supported: false,
|
|
83
119
|
reason: message.replace(sourcePath, filename),
|
|
120
|
+
suggestion: 'Use Workstar runtime compatibility for stateful or third-party components, or simplify this component for source compilation.',
|
|
84
121
|
};
|
|
85
122
|
}
|
|
86
123
|
}));
|
|
@@ -88,6 +125,15 @@ export async function auditForeignDirectory(sourceDirectory) {
|
|
|
88
125
|
total: entries.length,
|
|
89
126
|
supported: entries.filter((entry) => entry.supported).length,
|
|
90
127
|
entries,
|
|
128
|
+
dependencies: [...dependencyFiles]
|
|
129
|
+
.map(([name, files]) => ({
|
|
130
|
+
package: name,
|
|
131
|
+
files: [...files].sort(),
|
|
132
|
+
handling: /^(?:react|react-dom|react-router|react-router-dom|vue)$/.test(name)
|
|
133
|
+
? 'workstar-runtime-alias'
|
|
134
|
+
: 'requires-browser-verification',
|
|
135
|
+
}))
|
|
136
|
+
.sort((left, right) => left.package.localeCompare(right.package)),
|
|
91
137
|
};
|
|
92
138
|
}
|
|
93
139
|
/** Compile every view before writing any generated modules. */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { ComponentCompileError } from './errors.js';
|
|
3
|
+
const supported = new Map([
|
|
4
|
+
[
|
|
5
|
+
'react',
|
|
6
|
+
new Set([
|
|
7
|
+
'Children',
|
|
8
|
+
'Component',
|
|
9
|
+
'Fragment',
|
|
10
|
+
'StrictMode',
|
|
11
|
+
'Suspense',
|
|
12
|
+
'cloneElement',
|
|
13
|
+
'createContext',
|
|
14
|
+
'createElement',
|
|
15
|
+
'createRef',
|
|
16
|
+
'forwardRef',
|
|
17
|
+
'isValidElement',
|
|
18
|
+
'lazy',
|
|
19
|
+
'memo',
|
|
20
|
+
'useCallback',
|
|
21
|
+
'useContext',
|
|
22
|
+
'useDebugValue',
|
|
23
|
+
'useEffect',
|
|
24
|
+
'useId',
|
|
25
|
+
'useImperativeHandle',
|
|
26
|
+
'useLayoutEffect',
|
|
27
|
+
'useMemo',
|
|
28
|
+
'useReducer',
|
|
29
|
+
'useRef',
|
|
30
|
+
'useState',
|
|
31
|
+
'useSyncExternalStore',
|
|
32
|
+
]),
|
|
33
|
+
],
|
|
34
|
+
['react-dom', new Set(['createPortal', 'flushSync'])],
|
|
35
|
+
['react-dom/client', new Set(['createRoot', 'hydrateRoot'])],
|
|
36
|
+
['react-dom/server', new Set(['renderToStaticMarkup', 'renderToString'])],
|
|
37
|
+
[
|
|
38
|
+
'react-router',
|
|
39
|
+
new Set([
|
|
40
|
+
'BrowserRouter',
|
|
41
|
+
'Link',
|
|
42
|
+
'NavLink',
|
|
43
|
+
'Navigate',
|
|
44
|
+
'Outlet',
|
|
45
|
+
'Route',
|
|
46
|
+
'Routes',
|
|
47
|
+
'useLocation',
|
|
48
|
+
'useNavigate',
|
|
49
|
+
'useOutletContext',
|
|
50
|
+
'useParams',
|
|
51
|
+
'useSearchParams',
|
|
52
|
+
]),
|
|
53
|
+
],
|
|
54
|
+
]);
|
|
55
|
+
supported.set('react-router-dom', supported.get('react-router'));
|
|
56
|
+
function failUnsupported(sourceFile, node, moduleName, exportName) {
|
|
57
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
58
|
+
throw new ComponentCompileError(`${moduleName} export ${exportName} is not implemented by the Workstar runtime. ` +
|
|
59
|
+
`Supported exports: ${[...(supported.get(moduleName) ?? [])].join(', ')}.`, sourceFile.fileName, { line: position.line + 1, column: position.character + 1 });
|
|
60
|
+
}
|
|
61
|
+
/** Fail early with file and symbol context for unsupported compatibility APIs. */
|
|
62
|
+
export function validateReactRuntimeSource(source, filename) {
|
|
63
|
+
if (!/['"]react(?:-dom(?:\/(?:client|server))?|-router(?:-dom)?)?['"]/.test(source))
|
|
64
|
+
return;
|
|
65
|
+
const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(filename) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
66
|
+
const namespaces = new Map();
|
|
67
|
+
for (const statement of sourceFile.statements) {
|
|
68
|
+
if (!ts.isImportDeclaration(statement))
|
|
69
|
+
continue;
|
|
70
|
+
const moduleName = ts.isStringLiteral(statement.moduleSpecifier)
|
|
71
|
+
? statement.moduleSpecifier.text
|
|
72
|
+
: '';
|
|
73
|
+
const exports = supported.get(moduleName);
|
|
74
|
+
const clause = statement.importClause;
|
|
75
|
+
if (!exports || !clause || clause.isTypeOnly)
|
|
76
|
+
continue;
|
|
77
|
+
if (clause.name) {
|
|
78
|
+
if (moduleName === 'react' || moduleName === 'react-dom')
|
|
79
|
+
namespaces.set(clause.name.text, moduleName);
|
|
80
|
+
else
|
|
81
|
+
failUnsupported(sourceFile, clause.name, moduleName, 'default');
|
|
82
|
+
}
|
|
83
|
+
const bindings = clause.namedBindings;
|
|
84
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
85
|
+
namespaces.set(bindings.name.text, moduleName);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
89
|
+
continue;
|
|
90
|
+
for (const element of bindings.elements) {
|
|
91
|
+
if (element.isTypeOnly)
|
|
92
|
+
continue;
|
|
93
|
+
const exportName = element.propertyName?.text ?? element.name.text;
|
|
94
|
+
if (!exports.has(exportName))
|
|
95
|
+
failUnsupported(sourceFile, element, moduleName, exportName);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const visit = (node) => {
|
|
99
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
100
|
+
ts.isIdentifier(node.expression)) {
|
|
101
|
+
const moduleName = namespaces.get(node.expression.text);
|
|
102
|
+
if (moduleName && !supported.get(moduleName)?.has(node.name.text))
|
|
103
|
+
failUnsupported(sourceFile, node.name, moduleName, node.name.text);
|
|
104
|
+
}
|
|
105
|
+
ts.forEachChild(node, visit);
|
|
106
|
+
};
|
|
107
|
+
visit(sourceFile);
|
|
108
|
+
}
|
package/dist/src/vite.js
CHANGED
|
@@ -6,6 +6,7 @@ import { convertReactRootEntry } from './react-entry-compat.js';
|
|
|
6
6
|
import { reactComponentExportName } from './react-compat.js';
|
|
7
7
|
import { resolveReactComponentImport } from './react-import-resolution.js';
|
|
8
8
|
import { transpileComponent } from './source-map.js';
|
|
9
|
+
import { validateReactRuntimeSource } from './runtime-diagnostics.js';
|
|
9
10
|
/** Compile authored components as Vite modules without writing into src. */
|
|
10
11
|
export function workstar(options = {}) {
|
|
11
12
|
let sourceDirectory;
|
|
@@ -78,6 +79,9 @@ export function workstar(options = {}) {
|
|
|
78
79
|
? `${runtime}/client.js`
|
|
79
80
|
: `${runtime}/client`;
|
|
80
81
|
const dom = isAbsolute(runtime) ? `${runtime}/dom.js` : `${runtime}/dom`;
|
|
82
|
+
const server = isAbsolute(runtime)
|
|
83
|
+
? `${runtime}/server.js`
|
|
84
|
+
: `${runtime}/server`;
|
|
81
85
|
const router = isAbsolute(runtime)
|
|
82
86
|
? resolve(runtime, '../react-router/index.js')
|
|
83
87
|
: 'workstar/compat/react-router';
|
|
@@ -88,7 +92,9 @@ export function workstar(options = {}) {
|
|
|
88
92
|
},
|
|
89
93
|
resolve: {
|
|
90
94
|
alias: [
|
|
95
|
+
{ find: /^react-router-dom$/, replacement: router },
|
|
91
96
|
{ find: /^react-router$/, replacement: router },
|
|
97
|
+
{ find: /^react-dom\/server$/, replacement: server },
|
|
92
98
|
{ find: /^react-dom\/client$/, replacement: client },
|
|
93
99
|
{ find: /^react-dom$/, replacement: dom },
|
|
94
100
|
{ find: /^react\/jsx-dev-runtime$/, replacement: jsxDevRuntime },
|
|
@@ -139,6 +145,8 @@ export function workstar(options = {}) {
|
|
|
139
145
|
},
|
|
140
146
|
transform(source, id) {
|
|
141
147
|
const filename = id.split('?', 1)[0];
|
|
148
|
+
if (options.foreign === 'runtime' && /\.[cm]?[jt]sx?$/.test(filename))
|
|
149
|
+
validateReactRuntimeSource(source, filename);
|
|
142
150
|
if (options.foreign === 'automatic' && isForeignSource(filename)) {
|
|
143
151
|
const entry = filename.endsWith('.tsx')
|
|
144
152
|
? convertReactRootEntry(source, filename)
|