@gjsify/rolldown-plugin-gjsify 0.42.0 → 0.43.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/lib/app/browser.d.ts +2 -0
- package/lib/app/browser.js +4 -2
- package/lib/app/gjs.d.ts +2 -0
- package/lib/app/gjs.js +4 -2
- package/lib/app/nativescript.d.ts +2 -0
- package/lib/app/nativescript.js +4 -2
- package/lib/app/node.d.ts +2 -0
- package/lib/app/node.js +4 -2
- package/lib/library/lib.d.ts +2 -0
- package/lib/library/lib.js +12 -1
- package/lib/plugin.d.ts +15 -0
- package/lib/plugins/intl-segmenter-banner.d.ts +1 -0
- package/lib/plugins/intl-segmenter-banner.js +42 -0
- package/lib/plugins/process-stub.js +4 -0
- package/lib/utils/detect-free-globals.js +29 -7
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/jsx-survival.d.ts +31 -0
- package/lib/utils/jsx-survival.js +102 -0
- package/package.json +9 -8
package/lib/app/browser.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
|
|
2
2
|
import type { PluginOptions } from '../types/plugin-options.js';
|
|
3
3
|
export interface BrowserBuildConfig {
|
|
4
|
+
/** Transforms that must see the ORIGINAL source; composed before the caller's plugins. */
|
|
5
|
+
prePlugins: RolldownPluginOption[];
|
|
4
6
|
options: RolldownOptions;
|
|
5
7
|
plugins: RolldownPluginOption[];
|
|
6
8
|
}
|
package/lib/app/browser.js
CHANGED
|
@@ -86,11 +86,13 @@ export const setupForBrowser = async (input) => {
|
|
|
86
86
|
// the unresolved-workspace-import guard below so the guard reports the same
|
|
87
87
|
// promise the alias layer made.
|
|
88
88
|
const aliasEntries = flattenAliases(aliasMap);
|
|
89
|
+
// Reflection reads the ORIGINAL TypeScript, so it runs before any user
|
|
90
|
+
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
91
|
+
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
89
92
|
const plugins = [
|
|
90
93
|
gjsImportsEmptyPlugin(),
|
|
91
94
|
aliasPlugin({ entries: aliasEntries }),
|
|
92
95
|
blueprintPlugin(),
|
|
93
|
-
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
94
96
|
cssAsStringPlugin(),
|
|
95
97
|
// `order: 'post'` — see app/gjs.ts. The browser target's whole job is to
|
|
96
98
|
// replace Node builtins with their `@gjsify/*` browser entries; when one
|
|
@@ -103,7 +105,7 @@ export const setupForBrowser = async (input) => {
|
|
|
103
105
|
isExternal: (id) => external.includes(id),
|
|
104
106
|
}),
|
|
105
107
|
];
|
|
106
|
-
return { options, plugins };
|
|
108
|
+
return { options, prePlugins, plugins };
|
|
107
109
|
};
|
|
108
110
|
function flattenAliases(map) {
|
|
109
111
|
const out = {};
|
package/lib/app/gjs.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { PluginOptions } from '../types/plugin-options.js';
|
|
|
3
3
|
import type { GiSystemProbe } from '../plugins/gi-runtime-paths.js';
|
|
4
4
|
/** Resolved Rolldown configuration template + plugins for `--app gjs`. */
|
|
5
5
|
export interface GjsBuildConfig {
|
|
6
|
+
/** Transforms that must see the ORIGINAL source; composed before the caller's plugins. */
|
|
7
|
+
prePlugins: RolldownPluginOption[];
|
|
6
8
|
options: RolldownOptions;
|
|
7
9
|
plugins: RolldownPluginOption[];
|
|
8
10
|
}
|
package/lib/app/gjs.js
CHANGED
|
@@ -158,6 +158,9 @@ export const setupForGjs = async (input) => {
|
|
|
158
158
|
'random-access-file': 'random-access-file/index.js',
|
|
159
159
|
...flattenAliases(aliasMap),
|
|
160
160
|
};
|
|
161
|
+
// Reflection reads the ORIGINAL TypeScript, so it runs before any user
|
|
162
|
+
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
163
|
+
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
161
164
|
const plugins = [
|
|
162
165
|
// Virtual-entry plugin runs FIRST so its resolveId/load match the
|
|
163
166
|
// synthetic input ids that `wrapInputWithSideEffects` produces.
|
|
@@ -184,7 +187,6 @@ export const setupForGjs = async (input) => {
|
|
|
184
187
|
// first.
|
|
185
188
|
externalsPlugin(external, { name: 'gjsify-gjs-externalize' }),
|
|
186
189
|
blueprintPlugin(),
|
|
187
|
-
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
188
190
|
// GTK4's CSS engine is much older than browser engines — its
|
|
189
191
|
// parser predates nesting + many modern selectors. Targeting
|
|
190
192
|
// `firefox: 60 << 16` makes lightningcss flatten the source
|
|
@@ -218,7 +220,7 @@ export const setupForGjs = async (input) => {
|
|
|
218
220
|
: {}),
|
|
219
221
|
}),
|
|
220
222
|
];
|
|
221
|
-
return { options, plugins };
|
|
223
|
+
return { options, prePlugins, plugins };
|
|
222
224
|
};
|
|
223
225
|
/** Copy the alias map, dropping entries with an empty target. */
|
|
224
226
|
function flattenAliases(map) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
|
|
2
2
|
import type { PluginOptions } from '../types/plugin-options.js';
|
|
3
3
|
export interface NativescriptBuildConfig {
|
|
4
|
+
/** Transforms that must see the ORIGINAL source; composed before the caller's plugins. */
|
|
5
|
+
prePlugins: RolldownPluginOption[];
|
|
4
6
|
options: RolldownOptions;
|
|
5
7
|
plugins: RolldownPluginOption[];
|
|
6
8
|
}
|
package/lib/app/nativescript.js
CHANGED
|
@@ -124,6 +124,9 @@ export const setupForNativescript = async (input) => {
|
|
|
124
124
|
// the unresolved-workspace-import guard below so the guard reports the same
|
|
125
125
|
// promise the alias layer made.
|
|
126
126
|
const aliasEntries = flattenAliases(aliasMap);
|
|
127
|
+
// Reflection reads the ORIGINAL TypeScript, so it runs before any user
|
|
128
|
+
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
129
|
+
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
127
130
|
const plugins = [
|
|
128
131
|
gjsImportsEmptyPlugin(),
|
|
129
132
|
// Platform-specific source variants (`*.android` / `*.ios` /
|
|
@@ -135,7 +138,6 @@ export const setupForNativescript = async (input) => {
|
|
|
135
138
|
// NO cssAsStringPlugin — NS ships its own CSS pipeline via
|
|
136
139
|
// @nativescript/core; .css imports are handled by the consuming
|
|
137
140
|
// @nativescript/webpack or @nativescript/vite build
|
|
138
|
-
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
139
141
|
// `order: 'post'` — see app/gjs.ts. Native-bridge identifiers
|
|
140
142
|
// (`java.*`, `NS*`, …) are ambient GLOBALS on this target, never
|
|
141
143
|
// imports, so they never reach this hook; only a `@gjsify/*` edge does,
|
|
@@ -147,7 +149,7 @@ export const setupForNativescript = async (input) => {
|
|
|
147
149
|
isExternal,
|
|
148
150
|
}),
|
|
149
151
|
];
|
|
150
|
-
return { options, plugins };
|
|
152
|
+
return { options, prePlugins, plugins };
|
|
151
153
|
};
|
|
152
154
|
function flattenAliases(map) {
|
|
153
155
|
const out = {};
|
package/lib/app/node.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare function isGjsSourceBuild(options: {
|
|
|
54
54
|
registerInject?: string | undefined;
|
|
55
55
|
}): boolean;
|
|
56
56
|
export interface NodeBuildConfig {
|
|
57
|
+
/** Transforms that must see the ORIGINAL source; composed before the caller's plugins. */
|
|
58
|
+
prePlugins: RolldownPluginOption[];
|
|
57
59
|
options: RolldownOptions;
|
|
58
60
|
plugins: RolldownPluginOption[];
|
|
59
61
|
}
|
package/lib/app/node.js
CHANGED
|
@@ -206,6 +206,9 @@ export const setupForNode = async (input) => {
|
|
|
206
206
|
},
|
|
207
207
|
treeshake: true,
|
|
208
208
|
};
|
|
209
|
+
// Reflection reads the ORIGINAL TypeScript, so it runs before any user
|
|
210
|
+
// transform can strip the annotations out from under it (see `GjsifyConfig`).
|
|
211
|
+
const prePlugins = [deepkitPlugin({ reflection: input.pluginOptions.reflection })];
|
|
209
212
|
const plugins = [
|
|
210
213
|
// Virtual-entry plugin runs FIRST so its resolveId/load match the synthetic
|
|
211
214
|
// `\0gjsify-entry:` ids `wrapInputWithSideEffects` produces (no-op when
|
|
@@ -238,7 +241,6 @@ export const setupForNode = async (input) => {
|
|
|
238
241
|
// so a GJS app entry with composite-template windows must build for
|
|
239
242
|
// `--app node` too. Claims only `.blp` — inert for plain-Node bundles.
|
|
240
243
|
blueprintPlugin(),
|
|
241
|
-
deepkitPlugin({ reflection: input.pluginOptions.reflection }),
|
|
242
244
|
// On a node-gi build (REAL GTK on Node) lower the CSS to GTK 4's subset just
|
|
243
245
|
// as `--app gjs` does, so nesting/`&` is FLATTENED before it reaches
|
|
244
246
|
// `Gtk.CssProvider.load_from_string` — GTK 4's parser otherwise rejects
|
|
@@ -254,7 +256,7 @@ export const setupForNode = async (input) => {
|
|
|
254
256
|
// specifier at runtime.
|
|
255
257
|
unresolvedWorkspaceImportPlugin({ target: 'node', aliases: aliasEntries, isExternal: external }),
|
|
256
258
|
];
|
|
257
|
-
return { options, plugins };
|
|
259
|
+
return { options, prePlugins, plugins };
|
|
258
260
|
};
|
|
259
261
|
function flattenAliases(map) {
|
|
260
262
|
const out = {};
|
package/lib/library/lib.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RolldownOptions, RolldownPluginOption } from 'rolldown';
|
|
2
2
|
import type { PluginOptions } from '../types/plugin-options.js';
|
|
3
3
|
export interface LibBuildConfig {
|
|
4
|
+
/** Transforms that must see the ORIGINAL source; composed before the caller's plugins. */
|
|
5
|
+
prePlugins: RolldownPluginOption[];
|
|
4
6
|
options: RolldownOptions;
|
|
5
7
|
plugins: RolldownPluginOption[];
|
|
6
8
|
}
|
package/lib/library/lib.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// library config: consumers downstream (downstream apps using `gjsify
|
|
10
10
|
// build --app gjs|node|browser`) re-bundle and apply their own target
|
|
11
11
|
// lowering. Library output stays maximally portable.
|
|
12
|
+
import blueprintPlugin from '@gjsify/vite-plugin-blueprint';
|
|
12
13
|
import { aliasPlugin } from '../plugins/alias.js';
|
|
13
14
|
import { cssAsStringPlugin } from '../plugins/css-as-string.js';
|
|
14
15
|
import { externalsPlugin } from '../plugins/externals.js';
|
|
@@ -90,8 +91,18 @@ export const setupLib = async (input) => {
|
|
|
90
91
|
// tiny JS module re-exporting the CSS source, which preserveModules
|
|
91
92
|
// emits 1:1.
|
|
92
93
|
cssAsStringPlugin(),
|
|
94
|
+
// Same shape as the CSS hook above, and needed for the same reason: without it a `.blp`
|
|
95
|
+
// import reaches rolldown's JS parser, and a Blueprint file's first line — `using Gtk 4.0;`
|
|
96
|
+
// — is VALID JavaScript (a `using` resource declaration missing its initializer), so the
|
|
97
|
+
// build dies on "Using declarations must have an initializer" and nothing points at
|
|
98
|
+
// Blueprint. Only the app factories installed it, which quietly made Blueprint an
|
|
99
|
+
// application-only feature: a shared widget in a library package could not have a
|
|
100
|
+
// template, so its captions had to be assigned from TypeScript, where they carry no
|
|
101
|
+
// `translatable` attribute and no catalogue can ever reach them.
|
|
102
|
+
blueprintPlugin(),
|
|
93
103
|
];
|
|
94
|
-
|
|
104
|
+
// Library mode wires no reflection at all; the empty slot says so out loud.
|
|
105
|
+
return { options, prePlugins: [], plugins };
|
|
95
106
|
};
|
|
96
107
|
/**
|
|
97
108
|
* Whether a specifier should stay external in library mode: a bare /
|
package/lib/plugin.d.ts
CHANGED
|
@@ -3,6 +3,21 @@ import type { PluginOptions } from './types/plugin-options.js';
|
|
|
3
3
|
import type { GiSystemProbe } from './plugins/gi-runtime-paths.js';
|
|
4
4
|
export interface GjsifyConfig {
|
|
5
5
|
options: RolldownOptions;
|
|
6
|
+
/**
|
|
7
|
+
* Transforms that must see the ORIGINAL module source — composed BEFORE the
|
|
8
|
+
* caller's own plugins, while `plugins` stays after them.
|
|
9
|
+
*
|
|
10
|
+
* The split exists because the two positions answer different questions. User
|
|
11
|
+
* plugins go first so their `resolveId`/`load` wins over gjsify's alias and
|
|
12
|
+
* externals chain. But a user `transform: { order: 'pre' }` that REWRITES the
|
|
13
|
+
* source then also beats gjsify's own pre-transforms, and Deepkit's type
|
|
14
|
+
* compiler is one of them: `@gjsify/rolldown-plugin-solid` runs
|
|
15
|
+
* `@babel/preset-typescript` over every `.tsx`, so Deepkit was handed a file
|
|
16
|
+
* with no type annotations left. MEASURED on one `.tsx` carrying both a
|
|
17
|
+
* `typeOf<T>()` and a JSX tag: `const reflected = typeOf()` in the artifact,
|
|
18
|
+
* exit 0, not one diagnostic, and `Error: No type given` thrown at runtime.
|
|
19
|
+
*/
|
|
20
|
+
prePlugins: RolldownPluginOption[];
|
|
6
21
|
plugins: RolldownPluginOption[];
|
|
7
22
|
}
|
|
8
23
|
export interface GjsifyPluginInput {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const GJS_INTL_SEGMENTER_STUB: string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// One-line banner that supplies `Intl.Segmenter` on a GJS built without it.
|
|
2
|
+
//
|
|
3
|
+
// THE INCIDENT — measured, not hypothetical. `gjsify ship`'s first CI leg on a
|
|
4
|
+
// bare `ubuntu-latest` (the one that exists to hand a `.deb` to a real `dpkg`)
|
|
5
|
+
// installed the shipped package, ran the binary, and got:
|
|
6
|
+
//
|
|
7
|
+
// JS ERROR: TypeError: Intl.Segmenter is not a constructor
|
|
8
|
+
//
|
|
9
|
+
// Not an old runtime: that host had **gjs 1.88.1**, the same version as the
|
|
10
|
+
// Fedora container where everything passes. The difference is the ICU the two
|
|
11
|
+
// were built against, so a version number does not predict it — and the `.deb`
|
|
12
|
+
// this project ships declares `Depends: gjs >= 1.82`, which apt satisfies on
|
|
13
|
+
// that host. Install succeeds, program does not start: verbatim the failure
|
|
14
|
+
// class `depends.ts` was written to prevent, one layer below where it looks.
|
|
15
|
+
//
|
|
16
|
+
// WHY THE CLI NEEDS IT AT ALL, and why no application code is involved:
|
|
17
|
+
// `@gjsify/cli` → `yargs@18` → `string-width@7` → `get-east-asian-width`, which
|
|
18
|
+
// constructs a `Intl.Segmenter` AT MODULE SCOPE. It is the argument parser, so
|
|
19
|
+
// every command dies, `--help` included — reproduced locally by deleting
|
|
20
|
+
// `Intl.Segmenter` before importing the published bundle.
|
|
21
|
+
//
|
|
22
|
+
// WHAT THIS FALLBACK IS, AND IS NOT. It segments by CODE POINT, not by grapheme
|
|
23
|
+
// cluster, because a correct cluster algorithm is a Unicode table this banner
|
|
24
|
+
// cannot carry. Measured against native, via `string-width`:
|
|
25
|
+
//
|
|
26
|
+
// 'hello' 5→5 'あいう' 6→6 '🇩🇪' 2→2 'café' 4→4 (identical)
|
|
27
|
+
// '👨👩👧👦' 2→8 '👍🏽' 2→4 (over-counted)
|
|
28
|
+
//
|
|
29
|
+
// So ASCII, CJK, flags and combining accents are exact; ZWJ sequences and skin
|
|
30
|
+
// tone modifiers come out too wide. The cost is a misaligned column in terminal
|
|
31
|
+
// output containing those emoji. The cost of not having it is that nothing runs
|
|
32
|
+
// at all. It is installed ONLY when the constructor is missing, so on every GJS
|
|
33
|
+
// with a full ICU — every currently green CI leg — this line does nothing.
|
|
34
|
+
//
|
|
35
|
+
// Kept on a single line, like the sibling banners: the banner runs before any
|
|
36
|
+
// source-map-aware machinery, so a newline here shifts every bundle line by one.
|
|
37
|
+
export const GJS_INTL_SEGMENTER_STUB = 'if(typeof Intl.Segmenter!=="function")Intl.Segmenter=class Segmenter{' +
|
|
38
|
+
'constructor(){}' +
|
|
39
|
+
'resolvedOptions(){return{locale:"en",granularity:"grapheme"}}' +
|
|
40
|
+
'segment(input){const s=String(input);return{input:s,[Symbol.iterator]:function*(){' +
|
|
41
|
+
'let i=0;for(const c of s){yield{segment:c,index:i,input:s};i+=c.length}}}}' +
|
|
42
|
+
'};';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BUNDLE_URL_BANNER } from './bundle-url-banner.js';
|
|
2
|
+
import { GJS_INTL_SEGMENTER_STUB } from './intl-segmenter-banner.js';
|
|
2
3
|
import { GJS_WELLKNOWN_SYMBOLS_STUB } from './wellknown-symbols-banner.js';
|
|
3
4
|
import { giRuntimePathsStub } from './gi-runtime-paths.js';
|
|
4
5
|
// Every GJS ambient global is reached through `globalThis.` — NEVER as a bare
|
|
@@ -84,6 +85,9 @@ export function processStubPlugin(options = {}) {
|
|
|
84
85
|
// is single-line and idempotent.
|
|
85
86
|
const stub = (options.captureBundleUrl ? BUNDLE_URL_BANNER : '') +
|
|
86
87
|
GJS_WELLKNOWN_SYMBOLS_STUB +
|
|
88
|
+
// Before the process stub and before module init: `get-east-asian-width`
|
|
89
|
+
// constructs a Segmenter at module scope, so a later injection is too late.
|
|
90
|
+
GJS_INTL_SEGMENTER_STUB +
|
|
87
91
|
GJS_PROCESS_STUB +
|
|
88
92
|
giRuntimePathsStub(options.giRuntimePaths ?? [], options.giSystemProbes ?? []);
|
|
89
93
|
const banner = composeBanner(stub, options.userBanner ?? '');
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as acorn from 'acorn';
|
|
10
10
|
import * as walk from 'acorn-walk';
|
|
11
11
|
import { GJS_GLOBALS_MAP } from '@gjsify/resolve-npm/globals-map';
|
|
12
|
+
import { classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
12
13
|
const KNOWN_GLOBALS = new Set(Object.keys(GJS_GLOBALS_MAP));
|
|
13
14
|
/**
|
|
14
15
|
* `<host>.<method>` patterns that must inject a global even though the identifier
|
|
@@ -88,6 +89,33 @@ function extractBindingNames(node) {
|
|
|
88
89
|
return [];
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse one analysis chunk, and say what a JSX failure IS.
|
|
94
|
+
*
|
|
95
|
+
* This parse is the first thing a `--globals auto` build does with bundled output, so a
|
|
96
|
+
* `jsx: "preserve"` project with no framework compiler dies HERE — and acorn's own
|
|
97
|
+
* message is `Unexpected token (3:11)`, which names neither JSX, nor the file, nor a
|
|
98
|
+
* setting. Every other parse failure is re-thrown untouched: acorn trailing
|
|
99
|
+
* SpiderMonkey on new syntax is acorn's problem, not the project's, and claiming JSX
|
|
100
|
+
* for it would be a lie with a fix attached.
|
|
101
|
+
*/
|
|
102
|
+
function parseAnalysisChunk(code) {
|
|
103
|
+
try {
|
|
104
|
+
return acorn.parse(code, {
|
|
105
|
+
ecmaVersion: 'latest',
|
|
106
|
+
sourceType: 'module',
|
|
107
|
+
// Acorn rejects shebangs by default, and any project bundling its own CLI gets
|
|
108
|
+
// one hoisted to byte 0.
|
|
109
|
+
allowHashBang: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const jsx = classifyJsxParseFailure(code, err);
|
|
114
|
+
if (jsx === null)
|
|
115
|
+
throw err;
|
|
116
|
+
throw new Error(formatSurvivingJsx(jsx, 'the analysis bundle'), { cause: err });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
/**
|
|
92
120
|
* Core scanner behind `detectFreeGlobals` (web/Node globals map) and
|
|
93
121
|
* `detectGjsAmbientGlobals` (the `--app node` GJS-ambient set), parametrised so the node
|
|
@@ -108,13 +136,7 @@ function scanFreeGlobals(code, opts) {
|
|
|
108
136
|
const knownGlobals = opts.knownGlobals;
|
|
109
137
|
const methodMarkers = opts.methodMarkers ?? {};
|
|
110
138
|
const wasmMarkers = opts.wasmMarkers ?? {};
|
|
111
|
-
const ast =
|
|
112
|
-
ecmaVersion: 'latest',
|
|
113
|
-
sourceType: 'module',
|
|
114
|
-
// Acorn rejects shebangs by default, and any project bundling its own CLI gets
|
|
115
|
-
// one hoisted to byte 0.
|
|
116
|
-
allowHashBang: true,
|
|
117
|
-
});
|
|
139
|
+
const ast = parseAnalysisChunk(code);
|
|
118
140
|
// Pass 1: every declared name in the module.
|
|
119
141
|
const declaredNames = new Set();
|
|
120
142
|
walk.simple(ast, {
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { detectFreeGlobals } from './detect-free-globals.js';
|
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
8
|
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
|
+
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
11
|
+
export type { SurvivingJsx } from './jsx-survival.js';
|
package/lib/utils/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { detectFreeGlobals } from './detect-free-globals.js';
|
|
|
7
7
|
export { resolveGlobalsList, writeRegisterInjectFile } from './scan-globals.js';
|
|
8
8
|
export { inlineStaticReads, isAbsoluteFsPath } from './inline-static-reads.js';
|
|
9
9
|
export { GJSIFY_VIRTUAL_PREFIX, isGjsifyVirtualModuleId } from './virtual-module-id.js';
|
|
10
|
+
export { locateSurvivingJsx, classifyJsxParseFailure, formatSurvivingJsx } from './jsx-survival.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Where the parser stopped, and on what. */
|
|
2
|
+
export interface SurvivingJsx {
|
|
3
|
+
/** 1-based line, as acorn reports it. */
|
|
4
|
+
line: number;
|
|
5
|
+
/** 0-based column, as acorn reports it. */
|
|
6
|
+
column: number;
|
|
7
|
+
/** The text around the failure, trimmed so a minified bundle stays readable. */
|
|
8
|
+
excerpt: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Does `err` — a failure from parsing `code` — say that raw JSX survived? Returns the
|
|
12
|
+
* location when the position attributes it to JSX, `null` when it does not.
|
|
13
|
+
*
|
|
14
|
+
* Split from {@link locateSurvivingJsx} so a caller that ALREADY parsed (and threw) can
|
|
15
|
+
* attribute its own failure without parsing a second time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function classifyJsxParseFailure(code: string, err: unknown): SurvivingJsx | null;
|
|
18
|
+
/**
|
|
19
|
+
* Parse `code` as an ES module and report surviving JSX. `null` means either "parses
|
|
20
|
+
* fine" or "fails for a reason that is not JSX" — the two cases this must not conflate,
|
|
21
|
+
* because only the first is a healthy bundle and only the second is acorn's problem
|
|
22
|
+
* rather than the build's.
|
|
23
|
+
*/
|
|
24
|
+
export declare function locateSurvivingJsx(code: string): SurvivingJsx | null;
|
|
25
|
+
/**
|
|
26
|
+
* The actionable message: what the artifact does, what the runtime will do with it, and
|
|
27
|
+
* the two settings that fix it. Shared by the post-bundle artifact gate and the
|
|
28
|
+
* `--globals auto` analysis pass, which hits the same source with a bare
|
|
29
|
+
* "Unexpected token (3:11)" naming neither JSX nor a setting.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatSurvivingJsx(found: SurvivingJsx, label: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Raw JSX surviving into an emitted bundle — the syntax-level oracle, and why it is a
|
|
2
|
+
// syntax check rather than a pattern match.
|
|
3
|
+
//
|
|
4
|
+
// `jsx: "preserve"` is the setting `babel-preset-solid` and the Vue SFC compiler need:
|
|
5
|
+
// it tells oxc to leave JSX alone so the framework's own compiler can handle it. When
|
|
6
|
+
// that compiler is absent, raw JSX travels all the way into the `.gjs.mjs` and NOTHING
|
|
7
|
+
// between `gjsify build` and the artifact notices — rolldown emits it, the build exits
|
|
8
|
+
// 0, and GJS refuses the file at LOAD, before one line runs. MEASURED on a two-line
|
|
9
|
+
// `.tsx` built `--app gjs --globals none`: exit 0, `r=<box title="hi"/>` in the bundle,
|
|
10
|
+
// and `SyntaxError: expected expression, got '<'` under `gjs -m`.
|
|
11
|
+
//
|
|
12
|
+
// A pattern match is the wrong oracle in both directions: `<div` hits a string inside a
|
|
13
|
+
// test name, and misses `<Foo.Bar>`, `<>` and every tag the pattern did not think of.
|
|
14
|
+
// The right question is the one GJS asks — "does this parse as an ES module?" — so this
|
|
15
|
+
// asks acorn, the same parser the `--globals auto` detector already runs over bundled
|
|
16
|
+
// output.
|
|
17
|
+
//
|
|
18
|
+
// A parse failure ALONE is not attributed to JSX: acorn trails SpiderMonkey on new
|
|
19
|
+
// syntax, and blaming JSX for something else would send the reader after a setting that
|
|
20
|
+
// is already right. The attribution is the FAILURE POSITION — a `<` followed by an
|
|
21
|
+
// identifier start, `>` (fragment) or `/` (closing tag). That is where acorn stops on
|
|
22
|
+
// every JSX shape measured: `<box …/>`, `<box>hi</box>`, `<>…</>`, `<Foo.Bar x={1}>`,
|
|
23
|
+
// JSX as a call argument, and JSX in a multi-declarator `const`.
|
|
24
|
+
import * as acorn from 'acorn';
|
|
25
|
+
/**
|
|
26
|
+
* What may follow the `<` for the failure to read as JSX rather than as a broken
|
|
27
|
+
* comparison: a tag name, a fragment (`<>`) or a closing tag (`</`).
|
|
28
|
+
*/
|
|
29
|
+
const JSX_AFTER_ANGLE_RE = /^[A-Za-z_$>/]/;
|
|
30
|
+
/** Characters of context kept around the failure position. */
|
|
31
|
+
const EXCERPT_RADIUS = 60;
|
|
32
|
+
function excerptAt(code, pos) {
|
|
33
|
+
const from = Math.max(0, pos - EXCERPT_RADIUS);
|
|
34
|
+
const to = Math.min(code.length, pos + EXCERPT_RADIUS);
|
|
35
|
+
// Newlines and tabs would break the one-line-per-fact shape of the message.
|
|
36
|
+
const text = code.slice(from, to).replace(/[\r\n\t]+/g, ' ');
|
|
37
|
+
return `${from > 0 ? '…' : ''}${text}${to < code.length ? '…' : ''}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Does `err` — a failure from parsing `code` — say that raw JSX survived? Returns the
|
|
41
|
+
* location when the position attributes it to JSX, `null` when it does not.
|
|
42
|
+
*
|
|
43
|
+
* Split from {@link locateSurvivingJsx} so a caller that ALREADY parsed (and threw) can
|
|
44
|
+
* attribute its own failure without parsing a second time.
|
|
45
|
+
*/
|
|
46
|
+
export function classifyJsxParseFailure(code, err) {
|
|
47
|
+
const pos = err.pos;
|
|
48
|
+
const loc = err.loc;
|
|
49
|
+
if (typeof pos !== 'number')
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof loc?.line !== 'number' || typeof loc.column !== 'number')
|
|
52
|
+
return null;
|
|
53
|
+
if (code[pos] !== '<')
|
|
54
|
+
return null;
|
|
55
|
+
if (!JSX_AFTER_ANGLE_RE.test(code.slice(pos + 1, pos + 2)))
|
|
56
|
+
return null;
|
|
57
|
+
return { line: loc.line, column: loc.column, excerpt: excerptAt(code, pos) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse `code` as an ES module and report surviving JSX. `null` means either "parses
|
|
61
|
+
* fine" or "fails for a reason that is not JSX" — the two cases this must not conflate,
|
|
62
|
+
* because only the first is a healthy bundle and only the second is acorn's problem
|
|
63
|
+
* rather than the build's.
|
|
64
|
+
*/
|
|
65
|
+
export function locateSurvivingJsx(code) {
|
|
66
|
+
try {
|
|
67
|
+
acorn.parse(code, {
|
|
68
|
+
ecmaVersion: 'latest',
|
|
69
|
+
sourceType: 'module',
|
|
70
|
+
// Any project bundling its own CLI gets a shebang hoisted to byte 0.
|
|
71
|
+
allowHashBang: true,
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return classifyJsxParseFailure(code, err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The actionable message: what the artifact does, what the runtime will do with it, and
|
|
81
|
+
* the two settings that fix it. Shared by the post-bundle artifact gate and the
|
|
82
|
+
* `--globals auto` analysis pass, which hits the same source with a bare
|
|
83
|
+
* "Unexpected token (3:11)" naming neither JSX nor a setting.
|
|
84
|
+
*/
|
|
85
|
+
export function formatSurvivingJsx(found, label) {
|
|
86
|
+
return [
|
|
87
|
+
`gjsify build: ${label} still contains raw JSX at ${found.line}:${found.column} — it is not valid ` +
|
|
88
|
+
'JavaScript, so GJS aborts at load with "SyntaxError: expected expression, got \'<\'" before one ' +
|
|
89
|
+
'line runs.',
|
|
90
|
+
` ${found.excerpt}`,
|
|
91
|
+
'',
|
|
92
|
+
'JSX reaches an artifact only when the transform was told to PRESERVE it (`jsx: "preserve"` in ' +
|
|
93
|
+
'`gjsify.bundler.transform.jsx` or in tsconfig `compilerOptions.jsx`) and nothing compiled it ' +
|
|
94
|
+
'afterwards. Either:',
|
|
95
|
+
' - register the framework compiler that consumes preserved JSX (`babel-preset-solid` for Solid, the ' +
|
|
96
|
+
'Vue SFC compiler) under `gjsify.bundler.plugins`, or',
|
|
97
|
+
' - drop `preserve` and give the automatic runtime an import source that resolves AND runs: ' +
|
|
98
|
+
'`bundler.transform.jsx: { "importSource": "<pkg exporting ./jsx-runtime>" }`, or tsconfig ' +
|
|
99
|
+
'`"jsx": "react-jsx"` + `"jsxImportSource"`. Not `@gjsify/gtk-host` — its `/jsx-runtime` is a ' +
|
|
100
|
+
'TYPE surface and throws when called.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-plugin-gjsify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Rolldown / Rollup / Vite plugin orchestrator for GJS, Node, and Browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/gjsify/gjsify.git"
|
|
42
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
43
|
+
"directory": "packages/infra/rolldown-plugin-gjsify"
|
|
43
44
|
},
|
|
44
45
|
"bugs": {
|
|
45
46
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
@@ -55,11 +56,11 @@
|
|
|
55
56
|
],
|
|
56
57
|
"license": "MIT",
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@gjsify/console": "^0.
|
|
59
|
-
"@gjsify/resolve-npm": "^0.
|
|
60
|
-
"@gjsify/rolldown-plugin-deepkit": "^0.
|
|
61
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
62
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
59
|
+
"@gjsify/console": "^0.43.0",
|
|
60
|
+
"@gjsify/resolve-npm": "^0.43.0",
|
|
61
|
+
"@gjsify/rolldown-plugin-deepkit": "^0.43.0",
|
|
62
|
+
"@gjsify/rolldown-plugin-pnp": "^0.43.0",
|
|
63
|
+
"@gjsify/vite-plugin-blueprint": "^0.43.0",
|
|
63
64
|
"@rollup/pluginutils": "^5.4.0",
|
|
64
65
|
"acorn": "^8.17.0",
|
|
65
66
|
"acorn-typescript": "^1.4.13",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"sass": "^1.101.0"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
|
-
"@gjsify/lightningcss-native": "^0.
|
|
73
|
+
"@gjsify/lightningcss-native": "^0.43.0",
|
|
73
74
|
"rolldown": "^1.1.4"
|
|
74
75
|
},
|
|
75
76
|
"peerDependenciesMeta": {
|