@crossworks/app-build 0.239.8 → 0.239.9
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/package.json +2 -2
- package/src/index.ts +18 -2
- package/src/runtime-imports.test.ts +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossworks/app-build",
|
|
3
|
-
"version": "0.239.
|
|
3
|
+
"version": "0.239.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"src"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@mantle/share-ui": "npm:@crossworks/share-ui@0.239.
|
|
14
|
+
"@mantle/share-ui": "npm:@crossworks/share-ui@0.239.9",
|
|
15
15
|
"@tailwindcss/oxide": "^4.3.3",
|
|
16
16
|
"esbuild": "^0.28.2",
|
|
17
17
|
"lucide-react": "^1.41.0",
|
package/src/index.ts
CHANGED
|
@@ -219,15 +219,29 @@ export function lintToolRefs(source: AppSource, declaredSlugs: string[]): BuildM
|
|
|
219
219
|
* and named (including a default+named combination). */
|
|
220
220
|
const IMPORT_RE = /import\s+([^'";]+?)\s+from\s*['"]([^'"]+)['"]/g;
|
|
221
221
|
|
|
222
|
+
/** `import type …` — the whole clause is type-only. Lookahead keeps
|
|
223
|
+
* `import type from 'm'` and `import type, { a } from 'm'` (a default import
|
|
224
|
+
* that happens to be NAMED `type`) as value imports. */
|
|
225
|
+
const TYPE_ONLY_CLAUSE_RE = /^type(?=\s*[{*]|\s+[A-Za-z_$])/;
|
|
226
|
+
|
|
227
|
+
/** `{ type X }` / `{ type X as Y }` — one type-only specifier. `{ type }` and
|
|
228
|
+
* `{ type as t }` import a value named `type` and stay checked. */
|
|
229
|
+
const TYPE_ONLY_SPECIFIER_RE = /^type\s+(?!as\b)[A-Za-z_$]/;
|
|
230
|
+
|
|
222
231
|
/** Split an import clause into the names it binds FROM the module.
|
|
223
232
|
* `Foo` → default
|
|
224
233
|
* `* as ns` → namespace (binds nothing by name; nothing to check)
|
|
225
234
|
* `{ a, b as c }` → a, b
|
|
226
235
|
* `Foo, { a }` → default, a
|
|
227
|
-
*
|
|
236
|
+
* `type { A }` → nothing (type-only)
|
|
237
|
+
* `{ type A, b }` → b
|
|
238
|
+
* Returns the names as the MODULE must provide them, not the local aliases.
|
|
239
|
+
* Type-only imports and specifiers are skipped: the compiler erases them, so
|
|
240
|
+
* they never reach the runtime and cannot fail to link. */
|
|
228
241
|
function importedNames(clause: string): { names: string[]; namespace: boolean } {
|
|
229
242
|
const names: string[] = [];
|
|
230
243
|
let namespace = false;
|
|
244
|
+
if (TYPE_ONLY_CLAUSE_RE.test(clause)) return { names, namespace };
|
|
231
245
|
const braced = clause.match(/\{([^}]*)\}/);
|
|
232
246
|
const head = clause
|
|
233
247
|
.slice(0, braced ? clause.indexOf('{') : undefined)
|
|
@@ -239,6 +253,7 @@ function importedNames(clause: string): { names: string[]; namespace: boolean }
|
|
|
239
253
|
}
|
|
240
254
|
if (braced) {
|
|
241
255
|
for (const part of braced[1]!.split(',')) {
|
|
256
|
+
if (TYPE_ONLY_SPECIFIER_RE.test(part.trim())) continue;
|
|
242
257
|
const name = (part.split(/\s+as\s+/)[0] ?? '').trim();
|
|
243
258
|
if (name) names.push(name);
|
|
244
259
|
}
|
|
@@ -262,7 +277,8 @@ function importedNames(clause: string): { names: string[]; namespace: boolean }
|
|
|
262
277
|
* Errors, not warnings: the app is guaranteed not to run.
|
|
263
278
|
*
|
|
264
279
|
* Local/relative imports are the bundler's business and are skipped, as is a
|
|
265
|
-
* namespace import (`* as ns`), which binds no names and cannot mismatch
|
|
280
|
+
* namespace import (`* as ns`), which binds no names and cannot mismatch, and
|
|
281
|
+
* any type-only import or specifier, which the compiler erases.
|
|
266
282
|
*/
|
|
267
283
|
export function lintRuntimeImports(
|
|
268
284
|
source: AppSource,
|
|
@@ -71,6 +71,79 @@ describe('lintRuntimeImports', () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
describe('lintRuntimeImports: type-only imports', () => {
|
|
75
|
+
// The compiler erases these, so they never reach the runtime and cannot
|
|
76
|
+
// fail to link. The field case: an admin app's `import type { ReactNode }`.
|
|
77
|
+
const lint = (text: string) => lintRuntimeImports(src({ 'a.tsx': text }), RT);
|
|
78
|
+
|
|
79
|
+
it.each([
|
|
80
|
+
"import type { ReactNode } from 'react';\n",
|
|
81
|
+
"import type { ReactNode, FC as F } from 'react';\n",
|
|
82
|
+
"import type React from 'react';\n",
|
|
83
|
+
"import type * as R from 'react';\n",
|
|
84
|
+
"import type{ReactNode}from 'react';\n",
|
|
85
|
+
"import { type ReactNode } from 'react';\n",
|
|
86
|
+
"import { type ReactNode, type FC as F } from 'react';\n",
|
|
87
|
+
"import React, { type ReactNode } from 'react';\n",
|
|
88
|
+
"export type { ReactNode } from 'react';\n",
|
|
89
|
+
])('skips %s', (text) => {
|
|
90
|
+
expect(lint(text)).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('still checks the value part of a mixed import', () => {
|
|
94
|
+
expect(lint("import { type ReactNode, useState } from 'react';\n")).toEqual([]);
|
|
95
|
+
const msgs = lint("import { type ReactNode, nope } from 'react';\n");
|
|
96
|
+
expect(msgs).toHaveLength(1);
|
|
97
|
+
expect(msgs[0]!.text).toContain("'react' does not export 'nope'");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('treats a binding NAMED `type` as a value import', () => {
|
|
101
|
+
// `import type from` / `import type, {…}` = default import called `type`.
|
|
102
|
+
expect(lint("import type from '@host';\n")[0]!.text).toContain("'@host' has no default export");
|
|
103
|
+
expect(lint("import type, { host } from '@host';\n")).toHaveLength(1);
|
|
104
|
+
// `{ type }` / `{ type as t }` = named import of an export called `type`.
|
|
105
|
+
expect(lint("import { type } from '@host';\n")[0]!.text).toContain(
|
|
106
|
+
"'@host' does not export 'type'",
|
|
107
|
+
);
|
|
108
|
+
expect(lint("import { type as t } from '@host';\n")).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('buildApp: type-only imports', () => {
|
|
113
|
+
const app = (head: string) =>
|
|
114
|
+
src({ 'App.tsx': `${head}\nexport default function App(){return <div/>;}\n` });
|
|
115
|
+
|
|
116
|
+
it.each([
|
|
117
|
+
"import type { ReactNode } from 'react';",
|
|
118
|
+
"import { type ReactNode } from 'react';",
|
|
119
|
+
"import { type ReactNode, useState } from 'react'; console.log(useState);",
|
|
120
|
+
"export type { ReactNode } from 'react';",
|
|
121
|
+
])('builds with %s', async (head) => {
|
|
122
|
+
const res = await buildApp(app(head), { runtimeExports: RT });
|
|
123
|
+
expect(res.errors).toEqual([]);
|
|
124
|
+
expect(res.ok).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it.each([
|
|
128
|
+
"import type { ZodType } from 'zod';",
|
|
129
|
+
"import { type ZodType } from 'zod';",
|
|
130
|
+
"export type { ZodType } from 'zod';",
|
|
131
|
+
])('allows a disallowed package imported type-only: %s', async (head) => {
|
|
132
|
+
const res = await buildApp(app(head), { runtimeExports: RT });
|
|
133
|
+
expect(res.errors).toEqual([]);
|
|
134
|
+
expect(res.ok).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it.each([
|
|
138
|
+
"import { z } from 'zod'; console.log(z);",
|
|
139
|
+
"import { type ZodType, z } from 'zod'; console.log(z);",
|
|
140
|
+
])('refuses the same package imported as a value: %s', async (head) => {
|
|
141
|
+
const res = await buildApp(app(head), { runtimeExports: RT });
|
|
142
|
+
expect(res.ok).toBe(false);
|
|
143
|
+
expect(res.errors[0]!.text).toContain("Import 'zod' is not allowed in a mini app");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
74
147
|
describe('buildApp — runtime import check', () => {
|
|
75
148
|
it('FAILS the build rather than publishing an app that cannot link', async () => {
|
|
76
149
|
const res = await buildApp(
|