@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossworks/app-build",
3
- "version": "0.239.8",
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.8",
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
- * Returns the names as the MODULE must provide them, not the local aliases. */
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(