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 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.
@@ -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>;
@@ -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(await readFile(sourcePath, 'utf8'), sourcePath, {
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,2 @@
1
+ /** Fail early with file and symbol context for unsupported compatibility APIs. */
2
+ export declare function validateReactRuntimeSource(source: string, filename: string): void;
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workstar-compiler",
3
- "version": "0.2.4-beta.0",
3
+ "version": "0.2.4-beta.2",
4
4
  "description": "Component compiler and Vite plugin for Workstar applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",