@gjsify/rolldown-plugin-vue 0.42.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/README.md +156 -0
- package/lib/index.d.ts +72 -0
- package/lib/index.js +540 -0
- package/lib/source-map.d.ts +47 -0
- package/lib/source-map.js +206 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @gjsify/rolldown-plugin-vue
|
|
2
|
+
|
|
3
|
+
A Rolldown (and Rollup/Vite-compatible) plugin that compiles **Vue 3 single-file components** during gjsify builds, for a `@vue/runtime-core` custom renderer — the GTK one in [`@gjsify/gtk-host`](../../framework/gtk-host).
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
`@gjsify/gtk-host/vue` — the adapter — was complete and tested before this package. But it was tested through `h(...)` calls, which is to say through the renderer calls an SFC template compiles *to*. Nothing in the repository compiled a `.vue` file, so the compile step was unmeasured, and its failure mode is silent in the direction that matters: without `compilerOptions.isCustomElement`, **every** GTK tag compiles to `resolveComponent("gtk-box")`. Vue's resolver misses, warns once per tag — and the warning is `__DEV__`-only, which the production defines this pipeline requires strip.
|
|
10
|
+
|
|
11
|
+
Measured on one template, without the predicate: 7 of 7 tags became `_resolveComponent(…)` and **zero** element vnodes were emitted. With it: 0 `resolveComponent`, 6 `createElementVNode` plus the root `createElementBlock`.
|
|
12
|
+
|
|
13
|
+
`@vue/compiler-sfc` is loaded on the first `.vue` module, so a build with no SFC in it never pays for it.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gjsify install @gjsify/rolldown-plugin-vue
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Wire it through `package.json#gjsify`, no JS-form config file needed:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"gjsify": {
|
|
28
|
+
"bundler": {
|
|
29
|
+
"plugins": [{ "name": "@gjsify/rolldown-plugin-vue" }]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or directly in a Rolldown / Vite config:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { vuePlugin } from '@gjsify/rolldown-plugin-vue';
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
plugins: [vuePlugin()],
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
| Option | Default | Meaning |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `isCustomElement` | `isGtkHostTag` | Which tags compile to an element vnode instead of a component lookup. |
|
|
50
|
+
| `include` | `/\.vue$/` | Which modules to compile. A `g`/`y` flag is stripped: both make `RegExp.test` stateful, and this plugin tests three times per module — measured with `/\.vue$/g`, `resolveId` returned `null` for *every* `.vue` import and `load` alternated between compiling and declining. |
|
|
51
|
+
| `runtimeModuleName` | `@vue/runtime-core` | Module the generated code imports Vue's runtime from. `vue` — the compiler's own default — drags `@vue/runtime-dom` and the DOM renderer into a bundle with no DOM. |
|
|
52
|
+
|
|
53
|
+
### The default tag rule, and where its authority is
|
|
54
|
+
|
|
55
|
+
`isGtkHostTag` accepts `gtk-`/`adw-` kebab **and** `Gtk`/`Adw` + a capital. Both spellings are required: `isCustomElement` is consulted for PascalCase tags too (measured — it was asked about `GtkLabel`, `GtkBox` and `GtkGLArea` as well as the kebab forms), and one `GlobalComponents` key answers both spellings, so a kebab-only predicate leaves `<GtkBox>` type-checking and then resolving as a missing component.
|
|
56
|
+
|
|
57
|
+
It is a **prefix rule** on purpose. It decides "element vnode or component lookup", never whether the widget exists. An unknown tag is refused by name twice, and neither place is here:
|
|
58
|
+
|
|
59
|
+
- at render time, `@gjsify/gtk-host`'s registry throws `unknown-tag`;
|
|
60
|
+
- at type-check time, `GlobalComponents` + `strictTemplates` refuses it.
|
|
61
|
+
|
|
62
|
+
Encoding the widget list in this plugin would be a third copy of a generated table, and the first one to drift. The rule does cover that table exactly: all 164 GType keys in `gtk-host/src/generated/props.ts` match `^(Gtk|Adw)[A-Z]`.
|
|
63
|
+
|
|
64
|
+
## What it refuses, and what it ignores
|
|
65
|
+
|
|
66
|
+
**`<style>` is refused with a named error.** GTK styling is a `Gtk.CssProvider` concern — there is no element a CSS rule could attach to, and `<style scoped>` compiles to an attribute selector GTK4 CSS does not have. Compiling the rest and saying nothing would build an app that renders unstyled with nothing anywhere to read about it. Load the CSS yourself with `Gtk.CssProvider.load_from_string()` plus `Gtk.StyleContext.add_provider_for_display()`, and put the selector on the widget with `cssClasses`.
|
|
67
|
+
|
|
68
|
+
**`<script lang="jsx">` / `lang="tsx">` is refused with a named error.** The module id's extension is what selects rolldown's parser, and it is chosen before anything has read the file (see below), so it cannot depend on the block's `lang`. Write JSX in a `.tsx` file and compile it with [`@gjsify/rolldown-plugin-solid`](../rolldown-plugin-solid).
|
|
69
|
+
|
|
70
|
+
**A split SFC is refused with a named error.** Measured: `<template src="./tpl.html"/>` compiled to `function __sfc_render__() { return null }`, and `<script src="./s.js">` to `const __sfc__ = {}` with the external module never imported — a component that renders blank or carries none of its logic, at exit 0. (`<script setup src>` never reaches this plugin: `@vue/compiler-sfc`'s own `parse()` rejects it.) So is a `<template lang>` other than `html`: there is no template preprocessor here, and measured, `lang="pug"` compiled to a render function **returning the pug source as a text node**.
|
|
71
|
+
|
|
72
|
+
**Every DOM-only template feature is refused with a named error, and this is the largest refusal set.** `compileTemplate` runs on `@vue/compiler-dom` — `@vue/compiler-sfc` ships no other template compiler — so `DOMDirectiveTransforms` and `DOMNodeTransforms` are installed and all of these *compile*. Each then fails in a way that leaves a GTK app running:
|
|
73
|
+
|
|
74
|
+
| written | emitted | how it fails |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `v-show` | `vShow` + `withDirectives` | `vShow` is absent from `@vue/runtime-core` |
|
|
77
|
+
| `v-model` on an element | `vModelText` / `vModelCheckbox` / `vModelDynamic` | all three absent |
|
|
78
|
+
| `@x.stop`, `@x.enter` | `withModifiers`, `withKeys` | both absent |
|
|
79
|
+
| `<Transition>`, `<TransitionGroup>` (both spellings) | `Transition`, `TransitionGroup` | both absent |
|
|
80
|
+
| `v-html`, `v-text` | an `innerHTML` / `textContent` prop | no GTK widget has it |
|
|
81
|
+
| `class`, `:class`, `style`, `:style` | a `class` / `style` prop | no GTK widget has it; `cssClasses` is a string **array** |
|
|
82
|
+
|
|
83
|
+
The absent imports are a rolldown **MISSING_EXPORT warning at exit 0**, and the app then calls `undefined` inside a GLib callback. The dead props are refused by `@gjsify/gtk-host` as `unknown-prop` at *render* time — also inside a GLib callback, where GJS prints `JS ERROR` and the process still exits 0. Neither is a build failure without this refusal.
|
|
84
|
+
|
|
85
|
+
`v-model` is refused on an **element only**: measured, on a component it compiles to `modelValue` + `onUpdate:modelValue` and imports nothing DOM-shaped, so refusing it there would break legitimate Vue. `v-if`/`v-else`/`v-for`/`v-slot`/`v-once`/`v-cloak` and a plain `@signal` are untouched.
|
|
86
|
+
|
|
87
|
+
**Out of scope, and not attempted:** `<style>` including `scoped`, HMR, asset-URL rewriting (`transformAssetUrls` is off), custom blocks, SSR, `?query` suffixes on a `.vue` import, and watch-mode dependency registration. Custom blocks are the one thing that is neither compiled nor refused — they carry no runtime semantics of their own, so nothing in the bundle reads them and nothing renders differently.
|
|
88
|
+
|
|
89
|
+
## How a `.vue` id gets parsed
|
|
90
|
+
|
|
91
|
+
Rolldown picks a parser from the id's extension, and `.vue` is not one it knows: hand it TypeScript on a `.vue` id and the build dies with `[PARSE_ERROR] Missing initializer in const declaration` pointing into the `.vue` file. Three mechanisms were measured on the same fixture:
|
|
92
|
+
|
|
93
|
+
| mechanism | node engine | GJS engine |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `transform` returning `moduleType: 'ts'` | exit 0 | **exit 1** — `rolldown: unsupported moduleType 'ts'` |
|
|
96
|
+
| `moduleTypes: { '.vue': 'ts' }` input option | exit 0 | not reachable — the CLI has no passthrough |
|
|
97
|
+
| `resolveId` renaming to `App.vue.ts` + `load` | exit 0 | exit 0 |
|
|
98
|
+
|
|
99
|
+
So this plugin renames. `resolveId` resolves `./App.vue` normally (`skipSelf`) and appends `.ts` to the absolute path; `load` strips the suffix, reads the real file and compiles it. The real path stays the id's prefix, so a diagnostic still names the file.
|
|
100
|
+
|
|
101
|
+
`moduleType` is not an undocumented field — rolldown 1.1.4 ships it in `SourceDescription`, and it is the *designed* mechanism. It is unavailable here because `@gjsify/rolldown-native`'s `plugin_proxy.rs::parse_module_type` accepts `js`/`ecmascript`/`json`/`text` and rejects everything else, which makes it unusable on the primary target. That gap is recorded in `status/open-todos.md`; closing it is a prebuild-cycle change and this plugin does not depend on it.
|
|
102
|
+
|
|
103
|
+
One suffix and not four is why `lang="jsx"`/`"tsx"` is refused. A `<script>` with no `lang` is therefore parsed as TypeScript — harmless except for the handful of JS/TS syntactic ambiguities, and `lang="ts"` is what a project using this type surface writes anyway.
|
|
104
|
+
|
|
105
|
+
## TypeScript — both halves are required
|
|
106
|
+
|
|
107
|
+
The compile step above is one half. The other is `vue-tsc`, and it has a trap that produces a green check which verified nothing.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"compilerOptions": { "strict": true, "module": "NodeNext", "moduleResolution": "NodeNext" },
|
|
112
|
+
"vueCompilerOptions": { "strictTemplates": true },
|
|
113
|
+
"include": ["src/**/*.ts", "src/**/*.vue"]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- **`strictTemplates: true` is load-bearing.** Measured on `vue-tsc@3.3.11` against the `vue-host-counter` showcase: with it, an unknown prop, an unknown tag, an unknown event, a wrong value type and a bad enum nick all fail. Without it, the unknown prop and the unknown *tag* are silently accepted while wrong value types still error — so a project without it sees type errors appear and concludes the surface is working.
|
|
118
|
+
- **It must be set in the BASE of the `extends` chain.** Measured, four cells, all four: the base tsconfig's value wins and the child's is ignored outright, in *both* directions — a strict base stays strict under a child that sets `false`, and a lax base stays lax under a child that sets `true`. In a monorepo the shared base config therefore decides this for every package and a per-package override does nothing. `vue-tsc --showConfig` will not tell you either: measured, it prints `compilerOptions`, `files`, `include` and `exclude` and **not** `vueCompilerOptions`. So the config that sets it has to be the one with no `extends` above it — which is what `scripts/check-vue-program.mjs` asserts. This is the load-bearing warning in [`gtk-host/src/vue-components.ts`](../../framework/gtk-host/src/vue-components.ts).
|
|
119
|
+
- **`src/**/*.vue` must be in `include` explicitly, for the SFCs nothing imports.** A tsconfig whose `include` lists only `.ts` globs makes `vue-tsc` check **zero** *standalone* SFCs and exit 0. Measured, the limit of that: an SFC a root `.ts` imports is pulled into the program and fully type-checked either way, so the glob is what covers a component that is not wired up yet.
|
|
120
|
+
- **`vue-tsc --noEmit` on its own is an exit code and nothing else.** `scripts/check-vue-program.mjs` is what makes it a check: it runs the package's own `vue-tsc` with `--listFiles`, requires every `.vue` on disk to be in the program, and requires the config to carry `strictTemplates` with no `extends` above it. All three are A/B-proven able to fail; a bare `vue-tsc --noEmit` stays green through each.
|
|
121
|
+
- The `GlobalComponents` augmentation only applies to a program that loads it, so the project needs `import '@gjsify/gtk-host/vue-components';` somewhere — best in a `.d.ts`, since the module carries no runtime value.
|
|
122
|
+
|
|
123
|
+
## The build recipe
|
|
124
|
+
|
|
125
|
+
`@vue/runtime-core` is DOM-free in fact, but `--globals auto` is a static scan and injects a polyfill per identifier it sees in a dev-only branch. Four defines are required, or the bundle grows `gi://Gdk`, `GdkPixbuf`, `Pango` and `PangoCairo`:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
--define '__VUE_OPTIONS_API__=false'
|
|
129
|
+
--define '__VUE_PROD_DEVTOOLS__=false'
|
|
130
|
+
--define '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__=false'
|
|
131
|
+
--define 'process.env.NODE_ENV="production"'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`showcases/gtk/vue-host-counter` asserts `globalThis.document` is `undefined` on every launch, so losing the recipe fails the showcase rather than silently growing four typelib dependencies. The full measurement, including the one import the recipe cannot save (`Suspense`), is in [`gtk-host`'s README](../../framework/gtk-host/README.md).
|
|
135
|
+
|
|
136
|
+
## Determinism
|
|
137
|
+
|
|
138
|
+
The plugin pins the two `@vue/compiler-sfc` options whose defaults are `__DEV__`, i.e. the *bundler's* `process.env.NODE_ENV`, which no gjsify build sets: `comments: false` (measured, the same SFC emitted 4 `createCommentVNode` calls with NODE_ENV unset and 0 with `NODE_ENV=production`) and `hoistStatic: false`, which the adapter prescribes.
|
|
139
|
+
|
|
140
|
+
**`comments: false` has to be set TWICE, and pinning it only at parse time made the plugin non-deterministic within one process.** `parse()` LRU-caches the descriptor and `compileTemplate` marks the ast `transformed`, so the second compile of the same file takes `resolveTemplateAST`, which re-parses `inAST.source` using `compilerOptions` as the *parse* options. Measured on one fixture in one process: pass 1 emitted 0 `createCommentVNode` calls, pass 2 emitted 1, and the two modules differed byte-for-byte. So it is pinned in `templateParseOptions` **and** in `compilerOptions`, and a test compiles one fixture twice and asserts byte equality.
|
|
141
|
+
|
|
142
|
+
`transformHoist: null` used to sit beside `hoistStatic: false` with a comment calling it load-bearing. It was dead: `@vue/compiler-dom`'s `compile()` spreads its own `transformHoist: stringifyStatic` *after* the caller's options, so the `null` never reached `baseCompile`. Measured with `hoistStatic: true`, `null` and `undefined` produced byte-identical output with 1 `createStaticVNode(` each; only `hoistStatic: false` suppresses it. The option is gone and `hoistStatic: false` keeps its two tests.
|
|
143
|
+
|
|
144
|
+
One residual remains and has no option behind it: the generated code's patch-flag annotations (`8 /* PROPS */` vs `8`) and the `v-if` placeholder's debug text follow `__DEV__`. Neither changes a call or an argument that runs, and minification removes them.
|
|
145
|
+
|
|
146
|
+
## Source maps
|
|
147
|
+
|
|
148
|
+
`load` hands rolldown one map over the whole `.vue` file. Both halves of an SFC come with their own map and both already resolve to the *whole* file — measured, `sourcesContent[0]` is the SFC source in both, and the template map's source lines already count from the top of the file rather than from the template block — so only the generated side has to be fixed up, which is where the two halves landed in the joined module.
|
|
149
|
+
|
|
150
|
+
The two maps are decoded and re-encoded rather than spliced: the generated column resets per line, but the source index, source line, source column and name index are deltas carried across the entire map, so concatenating two `mappings` strings reads the second half's first segment relative to the first half's last and puts every remaining mapping somewhere else in the file. Tests resolve a position through the merged map (line **and** column) instead of asserting the `mappings` string, which would only pin the encoder against itself.
|
|
151
|
+
|
|
152
|
+
Note that gjsify's own build presets set `output.sourcemap: false` *after* spreading the caller's output options, so under `gjsify build --app <target>` the map is not written; it reaches a consumer through a plain Rolldown/Vite config.
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
import { type CombinedSourceMap } from './source-map.js';
|
|
3
|
+
export interface VuePluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Which tags compile to an ELEMENT vnode instead of a component lookup.
|
|
6
|
+
*
|
|
7
|
+
* Defaults to {@link isGtkHostTag}. Widen it for a project that registers
|
|
8
|
+
* widgets under another prefix; narrow it and a GTK tag silently becomes a
|
|
9
|
+
* component lookup that resolves to nothing.
|
|
10
|
+
*/
|
|
11
|
+
isCustomElement?: (tag: string) => boolean;
|
|
12
|
+
/** Which modules to compile. Defaults to `.vue`. */
|
|
13
|
+
include?: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* Module the generated code imports Vue's runtime from.
|
|
16
|
+
*
|
|
17
|
+
* Defaults to `@vue/runtime-core`, which is what the gjsify adapter builds on.
|
|
18
|
+
* `vue` — the compiler's own default — pulls `@vue/runtime-dom` and the DOM
|
|
19
|
+
* renderer into a bundle that has no DOM.
|
|
20
|
+
*/
|
|
21
|
+
runtimeModuleName?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The default tag rule: `gtk-`/`adw-` kebab, or `Gtk`/`Adw` + a capital.
|
|
25
|
+
*
|
|
26
|
+
* A PREFIX RULE, deliberately, and its authority lies elsewhere — this predicate
|
|
27
|
+
* only decides "element vnode or component lookup", never whether the widget
|
|
28
|
+
* exists. An unknown tag is refused BY NAME twice: `@gjsify/gtk-host`'s registry
|
|
29
|
+
* throws `unknown-tag` at render time, and `GlobalComponents` + `strictTemplates`
|
|
30
|
+
* refuses it at type-check. Encoding the widget list here would be a third copy of
|
|
31
|
+
* a generated table, and the first one to drift.
|
|
32
|
+
*
|
|
33
|
+
* It covers that table exactly: all 164 GType keys in
|
|
34
|
+
* `gtk-host/src/generated/props.ts` match `^(Gtk|Adw)[A-Z]`, so every kebab tag
|
|
35
|
+
* derived from them matches `^(gtk|adw)-`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const isGtkHostTag: (tag: string) => boolean;
|
|
38
|
+
/** The emitted module and one map for it, over the whole `.vue` file. */
|
|
39
|
+
export interface CompiledSfc {
|
|
40
|
+
code: string;
|
|
41
|
+
/** `null` only when neither half produced a map — never a stand-in for "unmapped". */
|
|
42
|
+
map: CombinedSourceMap | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compile one SFC's source to a component module.
|
|
46
|
+
*
|
|
47
|
+
* Split out of the hooks so the whole compile is reachable without a bundler —
|
|
48
|
+
* which is what makes it testable at all.
|
|
49
|
+
*/
|
|
50
|
+
export declare function compileSfc(source: string, filename: string, options: {
|
|
51
|
+
isCustomElement: (tag: string) => boolean;
|
|
52
|
+
runtimeModuleName: string;
|
|
53
|
+
/**
|
|
54
|
+
* Where a NAMED-but-not-fatal finding goes — a custom block, today.
|
|
55
|
+
*
|
|
56
|
+
* A custom block carries no runtime semantics of its own, so refusing one
|
|
57
|
+
* would break a `<docs>` block that harms nothing; dropping it in silence is
|
|
58
|
+
* the other half of the same mistake. It gets said out loud instead.
|
|
59
|
+
*/
|
|
60
|
+
onWarn?: (message: string) => void;
|
|
61
|
+
}): Promise<CompiledSfc>;
|
|
62
|
+
/**
|
|
63
|
+
* Compile Vue SFCs to a component module for a gjsify custom renderer.
|
|
64
|
+
*
|
|
65
|
+
* Wire it through `package.json#gjsify` so no JS-form config file is needed:
|
|
66
|
+
*
|
|
67
|
+
* ```json
|
|
68
|
+
* "gjsify": { "bundler": { "plugins": [{ "name": "@gjsify/rolldown-plugin-vue" }] } }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function vuePlugin(options?: VuePluginOptions): Plugin;
|
|
72
|
+
export default vuePlugin;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// Vue single-file components for Rolldown — the compile step a GTK/GJS Vue app
|
|
2
|
+
// needs, and the half that did not exist while the adapter did.
|
|
3
|
+
//
|
|
4
|
+
// `@gjsify/gtk-host/vue` is a `@vue/runtime-core` custom renderer, and it was
|
|
5
|
+
// exercised through the renderer calls an SFC template compiles TO, never through
|
|
6
|
+
// `@vue/compiler-sfc`. Nothing in the repository compiled a `.vue` file.
|
|
7
|
+
//
|
|
8
|
+
// ONE DECISION SHAPES THE WHOLE FILE, and it is not the obvious one. Rolldown picks
|
|
9
|
+
// a parser from the id's EXTENSION, and `.vue` is not one it knows: hand it
|
|
10
|
+
// TypeScript on a `.vue` id and the build dies with `[PARSE_ERROR] Missing
|
|
11
|
+
// initializer in const declaration` pointing INTO the `.vue` file. The designed fix
|
|
12
|
+
// is `moduleType` on a transform result — real, documented API in rolldown 1.1.4's
|
|
13
|
+
// `SourceDescription` — and it is unusable here: `@gjsify/rolldown-native`'s
|
|
14
|
+
// `plugin_proxy.rs::parse_module_type` accepts js/ecmascript/json/text and rejects
|
|
15
|
+
// the rest, so `moduleType: 'ts'` builds under Node and fails under GJS, the primary
|
|
16
|
+
// target. So this plugin renames the module id in `resolveId` and compiles in
|
|
17
|
+
// `load`, which is the only one of the three candidates measured working on BOTH
|
|
18
|
+
// engines. The table of what each did is in the README; the core gap is recorded in
|
|
19
|
+
// `status/open-todos.md`.
|
|
20
|
+
//
|
|
21
|
+
// Everything else worth knowing is at its call site, because each one is a way this
|
|
22
|
+
// compiles green and renders wrong.
|
|
23
|
+
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { combineSourceMaps } from './source-map.js';
|
|
25
|
+
/** The component object every emitted module builds and exports. */
|
|
26
|
+
const SFC_BINDING = '__sfc__';
|
|
27
|
+
/** The render function, renamed out of the way of a user's own `render`. */
|
|
28
|
+
const RENDER_BINDING = '__sfc_render__';
|
|
29
|
+
/**
|
|
30
|
+
* Appended to the resolved `.vue` path so rolldown's extension-based parser
|
|
31
|
+
* selection reaches TypeScript. Fact 5 above.
|
|
32
|
+
*
|
|
33
|
+
* Named rather than a bare `.ts` so no real file can collide with it: a project
|
|
34
|
+
* that genuinely has an `App.vue.ts` on disk would otherwise see this plugin claim
|
|
35
|
+
* its `load` and compile `App.vue` in its place.
|
|
36
|
+
*
|
|
37
|
+
* THE `.ts` TAIL IS LOAD-BEARING FOR A SECOND REASON, and it is not rolldown's.
|
|
38
|
+
* `@gjsify/rolldown-plugin-deepkit` filters on `/\.(m|c)?tsx?$/`, so this id lands
|
|
39
|
+
* inside it and Deepkit reflects an SFC's `<script setup>` — measured: an SFC
|
|
40
|
+
* carrying `typeOf<Reflected>()` emits its `__ΩReflected` table. Rename the tail to
|
|
41
|
+
* `.js` or `.mjs` and that filter stops matching, and reflection switches off for
|
|
42
|
+
* every `.vue` file in the project with no diagnostic anywhere — `typeOf()` with no
|
|
43
|
+
* argument throws `No type given` at runtime, from a build that exited 0.
|
|
44
|
+
*
|
|
45
|
+
* Written down because the coupling is invisible at both ends: nothing in the
|
|
46
|
+
* deepkit plugin knows this suffix exists, and a rename here looks like a cosmetic
|
|
47
|
+
* choice about a name only this file uses.
|
|
48
|
+
*/
|
|
49
|
+
const VIRTUAL_SUFFIX = '.gjsify-vue.ts';
|
|
50
|
+
const DEFAULT_INCLUDE = /\.vue$/;
|
|
51
|
+
const DEFAULT_RUNTIME_MODULE_NAME = '@vue/runtime-core';
|
|
52
|
+
/**
|
|
53
|
+
* The caller's `include` without the two flags that make `RegExp.test` STATEFUL.
|
|
54
|
+
*
|
|
55
|
+
* `g` and `y` both maintain `lastIndex` across calls, and this plugin tests three times
|
|
56
|
+
* per module — the specifier, the resolved id, and the filename in `load`. Measured with
|
|
57
|
+
* `vuePlugin({ include: /\.vue$/g })`: `resolveId` returned `null` for EVERY `.vue`
|
|
58
|
+
* import (the specifier matched and advanced `lastIndex`, so the resolved id then did
|
|
59
|
+
* not), and `load` alternated between compiling and declining. Stripped rather than
|
|
60
|
+
* refused: neither flag can mean anything to a `test`, so there is no intent to honour.
|
|
61
|
+
*/
|
|
62
|
+
function stateless(include) {
|
|
63
|
+
const flags = include.flags.replace(/[gy]/g, '');
|
|
64
|
+
return flags === include.flags ? include : new RegExp(include.source, flags);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The default tag rule: `gtk-`/`adw-` kebab, or `Gtk`/`Adw` + a capital.
|
|
68
|
+
*
|
|
69
|
+
* A PREFIX RULE, deliberately, and its authority lies elsewhere — this predicate
|
|
70
|
+
* only decides "element vnode or component lookup", never whether the widget
|
|
71
|
+
* exists. An unknown tag is refused BY NAME twice: `@gjsify/gtk-host`'s registry
|
|
72
|
+
* throws `unknown-tag` at render time, and `GlobalComponents` + `strictTemplates`
|
|
73
|
+
* refuses it at type-check. Encoding the widget list here would be a third copy of
|
|
74
|
+
* a generated table, and the first one to drift.
|
|
75
|
+
*
|
|
76
|
+
* It covers that table exactly: all 164 GType keys in
|
|
77
|
+
* `gtk-host/src/generated/props.ts` match `^(Gtk|Adw)[A-Z]`, so every kebab tag
|
|
78
|
+
* derived from them matches `^(gtk|adw)-`.
|
|
79
|
+
*/
|
|
80
|
+
export const isGtkHostTag = (tag) => /^(gtk|adw)-/.test(tag) || /^(Gtk|Adw)[A-Z]/.test(tag);
|
|
81
|
+
/**
|
|
82
|
+
* Loaded on first compile, not at import.
|
|
83
|
+
*
|
|
84
|
+
* Same contract as `@gjsify/rolldown-plugin-solid` and
|
|
85
|
+
* `@gjsify/rolldown-plugin-deepkit`: a build with no `.vue` in it never pays for
|
|
86
|
+
* `@vue/compiler-sfc` (which pulls in `@babel/parser`, `postcss` and
|
|
87
|
+
* `magic-string`), and under `--app gjs` the CLI bundles this plugin for GJS before
|
|
88
|
+
* importing it, so that whole tree has to load under GJS as well.
|
|
89
|
+
*/
|
|
90
|
+
let cached = null;
|
|
91
|
+
function load() {
|
|
92
|
+
cached ??= import('@vue/compiler-sfc');
|
|
93
|
+
return cached;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* FNV-1a over filename + source, hex.
|
|
97
|
+
*
|
|
98
|
+
* The SFC "id" is Vue's scope id: it keys `<style scoped>` attribute selectors and
|
|
99
|
+
* `v-bind()` CSS variables. Both are out of scope here, so all this value needs is
|
|
100
|
+
* to be stable and collision-free per module — and to be computed WITHOUT
|
|
101
|
+
* `node:crypto`, because this plugin runs inside a CLI that runs on GJS.
|
|
102
|
+
*/
|
|
103
|
+
function scopeId(filename, source) {
|
|
104
|
+
let hash = 0x811c9dc5;
|
|
105
|
+
const input = `${filename} ${source}`;
|
|
106
|
+
for (let i = 0; i < input.length; i++) {
|
|
107
|
+
hash ^= input.charCodeAt(i);
|
|
108
|
+
// The 32-bit FNV prime, multiplied through Math.imul so it stays an int32.
|
|
109
|
+
hash = Math.imul(hash, 0x01000193);
|
|
110
|
+
}
|
|
111
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Refuse a `<script lang>` this plugin cannot place behind a `.ts` id.
|
|
115
|
+
*
|
|
116
|
+
* The suffix is chosen in `resolveId`, before anything has read the file, so it
|
|
117
|
+
* cannot depend on the block's `lang` — and a JSX dialect needs a `.tsx`/`.jsx` id
|
|
118
|
+
* to parse at all. Refusing by name beats emitting code rolldown will then fail to
|
|
119
|
+
* parse with a message about the generated line rather than about the `lang`.
|
|
120
|
+
*/
|
|
121
|
+
function assertSupportedLang(lang, filename) {
|
|
122
|
+
if (lang === undefined || lang === 'js' || lang === 'ts')
|
|
123
|
+
return;
|
|
124
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} declares <script lang="${lang}">, which this plugin does not ` +
|
|
125
|
+
`compile. Supported: no lang, "js", "ts". A JSX dialect inside an SFC script would need the module id ` +
|
|
126
|
+
`to end in .jsx/.tsx, which is decided before the file is read; write JSX in a .tsx file and compile it ` +
|
|
127
|
+
`with @gjsify/rolldown-plugin-solid instead.`);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Refuse a `<style>` block by name rather than dropping it.
|
|
131
|
+
*
|
|
132
|
+
* GTK styling is a `Gtk.CssProvider` concern: there is no element a CSS rule could
|
|
133
|
+
* attach to here, and `<style scoped>` compiles to an attribute selector GTK4 CSS
|
|
134
|
+
* does not even have. Compiling the rest and saying nothing is the exact failure
|
|
135
|
+
* shape this repository keeps eliminating — the app builds, runs, looks wrong, and
|
|
136
|
+
* nothing anywhere mentions the stylesheet.
|
|
137
|
+
*/
|
|
138
|
+
function refuseStyleBlocks(styles, id) {
|
|
139
|
+
if (styles.length === 0)
|
|
140
|
+
return;
|
|
141
|
+
const scoped = styles.filter((style) => style.scoped === true).length;
|
|
142
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${id} has ${styles.length} <style> block(s)` +
|
|
143
|
+
`${scoped > 0 ? ` (${scoped} scoped)` : ''}, which this plugin does not compile. GTK styling goes ` +
|
|
144
|
+
`through a Gtk.CssProvider: load the CSS yourself with Gtk.CssProvider.load_from_string() plus ` +
|
|
145
|
+
`Gtk.StyleContext.add_provider_for_display(), and put the selector on the widget with cssClasses. ` +
|
|
146
|
+
`Dropping the block silently would build an app that renders unstyled with nothing to read about it.`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Refuse a `<template src>` / `<script src>` by name rather than compiling half a
|
|
150
|
+
* component.
|
|
151
|
+
*
|
|
152
|
+
* Measured: `<template src="./tpl.html"/>` beside a `<script setup>` compiles to
|
|
153
|
+
* `function __sfc_render__() { return null }`, and `<script src="./s.js">` beside a
|
|
154
|
+
* template compiles to `const __sfc__ = {}` with the external module never imported.
|
|
155
|
+
* Either way the app builds, runs, and renders a blank or logic-less component with
|
|
156
|
+
* nothing anywhere to read about it. `<script setup src>` never reaches here —
|
|
157
|
+
* `parse()` itself rejects it ("cannot use the src attribute because its syntax will be
|
|
158
|
+
* ambiguous outside of the component"), which `compileSfc` reports as an invalid SFC.
|
|
159
|
+
*/
|
|
160
|
+
function refuseExternalBlocks(blocks, filename) {
|
|
161
|
+
for (const [kind, block] of blocks) {
|
|
162
|
+
if (block?.src === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} has <${kind} src="${block.src}">, which this plugin does ` +
|
|
165
|
+
`not compile: it resolves no external block, so the ${kind} would be silently EMPTY — a component ` +
|
|
166
|
+
`that renders blank or carries none of its logic, at exit 0. Inline the ${kind} into the SFC.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Refuse a `<template lang>` this plugin does not run through a preprocessor.
|
|
171
|
+
*
|
|
172
|
+
* Nothing validated it before, and the failure is silent in the worst way: measured,
|
|
173
|
+
* `<template lang="pug">` compiles to a render function that RETURNS THE PUG SOURCE AS A
|
|
174
|
+
* TEXT NODE. The app builds and shows its own template markup.
|
|
175
|
+
*/
|
|
176
|
+
function assertSupportedTemplateLang(lang, filename) {
|
|
177
|
+
if (lang === undefined || lang === 'html')
|
|
178
|
+
return;
|
|
179
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} declares <template lang="${lang}">, which this plugin does not ` +
|
|
180
|
+
`compile — it runs no template preprocessor, and the block would be compiled as if it were HTML: ` +
|
|
181
|
+
`measured, a pug template became a render function returning the pug source as a text node. ` +
|
|
182
|
+
`Write the template as HTML.`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `NodeTypes.ELEMENT` / `NodeTypes.ATTRIBUTE` / `NodeTypes.DIRECTIVE` and
|
|
186
|
+
* `ElementTypes.ELEMENT`, from `@vue/compiler-core`'s enums.
|
|
187
|
+
*
|
|
188
|
+
* Spelled as literals because `@vue/compiler-sfc` re-exports neither enum, and taking
|
|
189
|
+
* `@vue/compiler-core` as a second direct dependency for four integers would also put it
|
|
190
|
+
* in the tree this plugin loads lazily. Each one is load-bearing for a refusal below, so
|
|
191
|
+
* a renumbering upstream turns those specs RED rather than turning the refusals off.
|
|
192
|
+
*/
|
|
193
|
+
const NODE_ELEMENT = 1;
|
|
194
|
+
const PROP_ATTRIBUTE = 6;
|
|
195
|
+
const PROP_DIRECTIVE = 7;
|
|
196
|
+
const TAG_PLAIN_ELEMENT = 0;
|
|
197
|
+
/** How the `@vue/runtime-dom` half of a feature would have failed on GTK. */
|
|
198
|
+
const MISSING_EXPORT = 'is a @vue/runtime-dom export, which @vue/runtime-core does not have: rolldown reports the missing import ' +
|
|
199
|
+
'as a WARNING at exit 0 and the app then calls `undefined` inside a GLib callback';
|
|
200
|
+
const UNKNOWN_PROP = 'compiles to a DOM prop no GTK widget has, which @gjsify/gtk-host refuses as `unknown-prop` at RENDER time ' +
|
|
201
|
+
'— inside a GLib callback, where GJS prints `JS ERROR` and the process still exits 0';
|
|
202
|
+
/**
|
|
203
|
+
* `v-<name>` → why it cannot compile here, and what to write instead.
|
|
204
|
+
*
|
|
205
|
+
* `plainElementsOnly` spares the legitimate use: measured, `v-model` on a COMPONENT
|
|
206
|
+
* compiles to `modelValue` + `onUpdate:modelValue` props and imports nothing DOM-shaped,
|
|
207
|
+
* while `v-model` on an element reaches for `vModelText`/`vModelCheckbox`/`vModelDynamic`.
|
|
208
|
+
*/
|
|
209
|
+
const DOM_ONLY_DIRECTIVES = [
|
|
210
|
+
{
|
|
211
|
+
name: 'show',
|
|
212
|
+
plainElementsOnly: false,
|
|
213
|
+
why: `\`vShow\` ${MISSING_EXPORT}. It toggles \`style.display\`, which GTK has no equivalent for: use ` +
|
|
214
|
+
`\`v-if\`, or bind the widget's own \`visible\` property.`,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'model',
|
|
218
|
+
plainElementsOnly: true,
|
|
219
|
+
why: `\`vModelText\`/\`vModelCheckbox\`/\`vModelDynamic\` ${MISSING_EXPORT}. Bind the widget property and ` +
|
|
220
|
+
`write it back from the widget's own signal: \`<gtk-entry :text="name" @changed="…" />\`.`,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'html',
|
|
224
|
+
plainElementsOnly: false,
|
|
225
|
+
why: `\`v-html\` ${UNKNOWN_PROP} (\`innerHTML\`). GTK parses no HTML — put the text in the widget's own ` +
|
|
226
|
+
`\`label\` property, or use Pango markup with \`useMarkup\`.`,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'text',
|
|
230
|
+
plainElementsOnly: false,
|
|
231
|
+
why: `\`v-text\` ${UNKNOWN_PROP} (\`textContent\`). Write the text as a child — ` +
|
|
232
|
+
`\`<gtk-label>{{ text }}</gtk-label>\` — or bind the widget's \`label\` property.`,
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
/**
|
|
236
|
+
* DOM props whose NAME survives compilation but which no GTK widget has.
|
|
237
|
+
*
|
|
238
|
+
* `style` reaches this list as a `v-bind`, not as an attribute: compiler-dom's
|
|
239
|
+
* `transformStyle` rewrites `style="color: red"` into a bound style OBJECT before any
|
|
240
|
+
* directive transform runs, so the static and bound spellings are indistinguishable here
|
|
241
|
+
* — and both are refused. `class` stays a plain attribute.
|
|
242
|
+
*/
|
|
243
|
+
const DOM_ONLY_PROPS = new Map([
|
|
244
|
+
[
|
|
245
|
+
'class',
|
|
246
|
+
`\`class\` ${UNKNOWN_PROP}. The GTK property is \`cssClasses\`, a string ARRAY: ` +
|
|
247
|
+
`\`<gtk-box :css-classes="['card']" />\`.`,
|
|
248
|
+
],
|
|
249
|
+
[
|
|
250
|
+
'style',
|
|
251
|
+
`\`style\` ${UNKNOWN_PROP}. GTK styling is a \`Gtk.CssProvider\` concern: load the CSS with ` +
|
|
252
|
+
`\`Gtk.CssProvider.load_from_string()\` plus \`Gtk.StyleContext.add_provider_for_display()\` and put ` +
|
|
253
|
+
`the selector on the widget with \`cssClasses\`.`,
|
|
254
|
+
],
|
|
255
|
+
]);
|
|
256
|
+
/**
|
|
257
|
+
* `<Transition>`/`<TransitionGroup>` in both spellings compiler-dom recognises.
|
|
258
|
+
*
|
|
259
|
+
* Measured: each emits `import { Transition } from "@vue/runtime-core"`, and both names
|
|
260
|
+
* are ABSENT there — they live in `@vue/runtime-dom`, driven by CSS transition classes.
|
|
261
|
+
* `@gjsify/gtk-host`'s README records the same fact from the runtime side.
|
|
262
|
+
*/
|
|
263
|
+
const DOM_ONLY_TAGS = new Map([
|
|
264
|
+
['Transition', 'Transition'],
|
|
265
|
+
['transition', 'Transition'],
|
|
266
|
+
['TransitionGroup', 'TransitionGroup'],
|
|
267
|
+
['transition-group', 'TransitionGroup'],
|
|
268
|
+
]);
|
|
269
|
+
/**
|
|
270
|
+
* Refuse the template features that only mean something against a DOM, BY NAME.
|
|
271
|
+
*
|
|
272
|
+
* `compileTemplate` runs on `@vue/compiler-dom` — `@vue/compiler-sfc` ships no other
|
|
273
|
+
* template compiler — so `DOMDirectiveTransforms` and `DOMNodeTransforms` are installed
|
|
274
|
+
* and every feature below COMPILES. Each then fails in one of two ways, and neither stops
|
|
275
|
+
* the app: an import `@vue/runtime-core` does not export, or a DOM prop no GTK widget
|
|
276
|
+
* has. Both are spelled out in the messages above, because the point of refusing is that
|
|
277
|
+
* someone reads why.
|
|
278
|
+
*
|
|
279
|
+
* A `nodeTransform` and not four `directiveTransforms`: two of these are not directives
|
|
280
|
+
* at all (`transformStyle` keys off an attribute, `transformTransition` off the tag), and
|
|
281
|
+
* an event MODIFIER is a property of a `v-on` this plugin must otherwise keep working.
|
|
282
|
+
* One hook sees all of them on ENTRY, before `buildProps` runs on exit.
|
|
283
|
+
*/
|
|
284
|
+
function refuseDomOnlyTemplateFeatures(node, filename) {
|
|
285
|
+
// The compiler hands its own AST to a `nodeTransform` and `@vue/compiler-sfc` exports
|
|
286
|
+
// no types for it, so the shape is asserted here rather than imported.
|
|
287
|
+
const element = node;
|
|
288
|
+
if (element.type !== NODE_ELEMENT)
|
|
289
|
+
return;
|
|
290
|
+
const tag = element.tag ?? '<unknown>';
|
|
291
|
+
const builtIn = DOM_ONLY_TAGS.get(tag);
|
|
292
|
+
if (builtIn !== undefined) {
|
|
293
|
+
refuse(filename, `<${tag}>`, `\`${builtIn}\` ${MISSING_EXPORT}. It animates CSS transition classes; a GTK animation is ` +
|
|
294
|
+
`\`Adw.TimedAnimation\` or a widget's own transition property (\`Gtk.Stack.transitionType\`).`);
|
|
295
|
+
}
|
|
296
|
+
for (const prop of element.props ?? []) {
|
|
297
|
+
if (prop.type === PROP_ATTRIBUTE) {
|
|
298
|
+
const why = DOM_ONLY_PROPS.get(prop.name);
|
|
299
|
+
if (why !== undefined)
|
|
300
|
+
refuse(filename, `\`${prop.name}\` on <${tag}>`, why);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (prop.type !== PROP_DIRECTIVE)
|
|
304
|
+
continue;
|
|
305
|
+
const directive = DOM_ONLY_DIRECTIVES.find((candidate) => candidate.name === prop.name);
|
|
306
|
+
if (directive !== undefined && (!directive.plainElementsOnly || element.tagType === TAG_PLAIN_ELEMENT)) {
|
|
307
|
+
refuse(filename, `\`v-${prop.name}\` on <${tag}>`, directive.why);
|
|
308
|
+
}
|
|
309
|
+
if (prop.name === 'bind' && prop.arg?.isStatic === true) {
|
|
310
|
+
const why = DOM_ONLY_PROPS.get(String(prop.arg.content));
|
|
311
|
+
if (why !== undefined)
|
|
312
|
+
refuse(filename, `\`:${String(prop.arg.content)}\` on <${tag}>`, why);
|
|
313
|
+
}
|
|
314
|
+
const modifiers = prop.name === 'on' ? (prop.modifiers ?? []) : [];
|
|
315
|
+
if (modifiers.length > 0) {
|
|
316
|
+
const spelling = modifiers.map((modifier) => `.${String(modifier.content)}`).join('');
|
|
317
|
+
refuse(filename, `\`@${String(prop.arg?.content ?? '')}${spelling}\` on <${tag}>`, `an event modifier compiles to \`withModifiers\`/\`withKeys\`, and both ${MISSING_EXPORT}. ` +
|
|
318
|
+
`They are DOM event plumbing (\`stopPropagation\`, \`event.key\`) — a GTK signal has neither. ` +
|
|
319
|
+
`Drop the modifier and do the check inside the handler.`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function refuse(filename, what, why) {
|
|
324
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} uses ${what}, which this plugin does not compile. ${why}`);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* `export function render` becomes `function __sfc_render__`, so the emitted module
|
|
328
|
+
* can hold a user's own `render` binding beside it.
|
|
329
|
+
*
|
|
330
|
+
* A string rewrite, like `@vitejs/plugin-vue`'s, because `compileTemplate` has no
|
|
331
|
+
* rename option — but ASSERTED, because a silent no-op here would leave a second
|
|
332
|
+
* `render` in the module (a redeclaration at best, a shadowed user binding at worst)
|
|
333
|
+
* and the failure would surface in the app rather than in the build.
|
|
334
|
+
*/
|
|
335
|
+
function renameRenderExport(code, id) {
|
|
336
|
+
const renamed = code.replace(/\nexport function render\(/, `\nfunction ${RENDER_BINDING}(`);
|
|
337
|
+
if (renamed === code) {
|
|
338
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: could not find the generated \`export function render(\` in the ` +
|
|
339
|
+
`template output for ${id}. @vue/compiler-sfc changed its codegen shape; this plugin renames ` +
|
|
340
|
+
`that export so it cannot collide with a user's own \`render\`.`);
|
|
341
|
+
}
|
|
342
|
+
return renamed;
|
|
343
|
+
}
|
|
344
|
+
/** Every message an SFC/template compile can report, flattened for one throw. */
|
|
345
|
+
function messagesOf(errors) {
|
|
346
|
+
return errors.map((error) => String(error.message ?? error)).join('\n ');
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Compile one SFC's source to a component module.
|
|
350
|
+
*
|
|
351
|
+
* Split out of the hooks so the whole compile is reachable without a bundler —
|
|
352
|
+
* which is what makes it testable at all.
|
|
353
|
+
*/
|
|
354
|
+
export async function compileSfc(source, filename, options) {
|
|
355
|
+
const { parse, compileScript, compileTemplate } = await load();
|
|
356
|
+
const { isCustomElement, runtimeModuleName } = options;
|
|
357
|
+
// BOTH options LOOK like codegen options and are really PARSER options, and
|
|
358
|
+
// setting them where they look like they belong is a silent no-op — a tag's kind
|
|
359
|
+
// (element vs component) and whether a comment survives are decided at parse
|
|
360
|
+
// time. `@vitejs/plugin-vue` hands `compileTemplate` the `ast` from a plain
|
|
361
|
+
// `parse()` and relies on Vite to have configured the parser; doing that here
|
|
362
|
+
// measured 2 of 2 tags back to `_resolveComponent` while
|
|
363
|
+
// `compilerOptions.isCustomElement` sat there set and ignored.
|
|
364
|
+
//
|
|
365
|
+
// That no-op also FAKED ITS OWN A/B: `compileTemplate` re-parses an ast that has
|
|
366
|
+
// already been transformed, so a loop compiling the SAME descriptor three times
|
|
367
|
+
// showed `comments: false` "working" on the second pass. Every measurement in
|
|
368
|
+
// this package is from a fresh process.
|
|
369
|
+
//
|
|
370
|
+
// `comments` is here for a second reason: it defaults to `__DEV__`, i.e. to the
|
|
371
|
+
// BUNDLER's `process.env.NODE_ENV`, which no build here sets. Measured, the same
|
|
372
|
+
// SFC emitted 4 `createCommentVNode` calls with NODE_ENV unset and 0 with
|
|
373
|
+
// NODE_ENV=production — a different bundle for an environment variable nothing
|
|
374
|
+
// declares. It has to be set in `compilerOptions` AS WELL, and pinning it here
|
|
375
|
+
// alone made the plugin non-deterministic WITHIN one process: `parse()` LRU-caches
|
|
376
|
+
// the descriptor, `compileTemplate` marks the ast `transformed`, and the next
|
|
377
|
+
// compile of the same file therefore takes `resolveTemplateAST`, which re-parses
|
|
378
|
+
// `inAST.source` using `compilerOptions` as the PARSE options. Measured on one
|
|
379
|
+
// fixture in one process: pass 1 emitted 0 `createCommentVNode` calls, pass 2
|
|
380
|
+
// emitted 1, and the two modules differed.
|
|
381
|
+
const { descriptor, errors } = parse(source, {
|
|
382
|
+
filename,
|
|
383
|
+
templateParseOptions: { isCustomElement, comments: false },
|
|
384
|
+
});
|
|
385
|
+
if (errors.length > 0) {
|
|
386
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} is not a valid SFC:\n ${messagesOf(errors)}`);
|
|
387
|
+
}
|
|
388
|
+
refuseStyleBlocks(descriptor.styles, filename);
|
|
389
|
+
refuseExternalBlocks([
|
|
390
|
+
['template', descriptor.template],
|
|
391
|
+
['script', descriptor.script],
|
|
392
|
+
], filename);
|
|
393
|
+
assertSupportedTemplateLang(descriptor.template?.lang ?? undefined, filename);
|
|
394
|
+
for (const block of descriptor.customBlocks) {
|
|
395
|
+
options.onWarn?.(`@gjsify/rolldown-plugin-vue: ${filename} carries a <${block.type}> block, which this plugin does ` +
|
|
396
|
+
`not compile. Nothing in the bundle will read it.`);
|
|
397
|
+
}
|
|
398
|
+
const hasScript = descriptor.script !== null || descriptor.scriptSetup !== null;
|
|
399
|
+
assertSupportedLang(descriptor.scriptSetup?.lang ?? descriptor.script?.lang ?? undefined, filename);
|
|
400
|
+
const compilerOptions = {
|
|
401
|
+
isCustomElement,
|
|
402
|
+
runtimeModuleName,
|
|
403
|
+
// The adapter's contract, and it prevents a real throw rather than merely
|
|
404
|
+
// honouring a prescription. `hoistStatic` (on by default) enables
|
|
405
|
+
// compiler-dom's `stringifyStatic`, which turns a large enough static
|
|
406
|
+
// subtree into `createStaticVNode("<html…>")` — and the adapter's
|
|
407
|
+
// `insertStaticContent` THROWS, because GTK parses no HTML. It reaches GTK
|
|
408
|
+
// tags too: MEASURED, 22 `<gtk-label title="t">x</gtk-label>` children
|
|
409
|
+
// stringified exactly like 22 `<div>`s. (A first measurement with 22
|
|
410
|
+
// self-closing, text-free labels did NOT stringify, and reading that as
|
|
411
|
+
// "custom elements are exempt" is the kind of near-miss this comment exists
|
|
412
|
+
// to stop.) `cloneNode` is the same class one step further out: a GObject
|
|
413
|
+
// does not clone.
|
|
414
|
+
hoistStatic: false,
|
|
415
|
+
// Not `transformHoist: null` beside it: compiler-dom's `compile()` spreads its
|
|
416
|
+
// own `transformHoist: stringifyStatic` AFTER the caller's options, so the null
|
|
417
|
+
// never reached `baseCompile`. Measured with `hoistStatic: true`, `null` and
|
|
418
|
+
// `undefined` emitted the same 1 `createStaticVNode(` and byte-identical output;
|
|
419
|
+
// `hoistStatic: false` is what actually suppresses it.
|
|
420
|
+
comments: false,
|
|
421
|
+
// The refusals this plugin owns, reached the only way a caller can reach them:
|
|
422
|
+
// compiler-dom appends `options.nodeTransforms` after its own, so this runs on
|
|
423
|
+
// entry to every element, before `buildProps` runs on exit.
|
|
424
|
+
nodeTransforms: [(node) => refuseDomOnlyTemplateFeatures(node, filename)],
|
|
425
|
+
};
|
|
426
|
+
const id = scopeId(filename, source);
|
|
427
|
+
// `genDefaultAs` turns `export default {…}` into a binding this module can then
|
|
428
|
+
// attach `render` to, and gives a script with no default export an empty
|
|
429
|
+
// component instead of a syntax error.
|
|
430
|
+
const script = hasScript
|
|
431
|
+
? compileScript(descriptor, {
|
|
432
|
+
id,
|
|
433
|
+
genDefaultAs: SFC_BINDING,
|
|
434
|
+
// `templateOptions` IS how `runtimeModuleName` reaches this call, and
|
|
435
|
+
// there is no other way in: `compileScript` reads it from
|
|
436
|
+
// `options.templateOptions.compilerOptions.runtimeModuleName` and
|
|
437
|
+
// otherwise emits `import { defineComponent } from 'vue'` — the full
|
|
438
|
+
// Vue package, which re-exports `@vue/runtime-dom` and its DOM
|
|
439
|
+
// renderer into a bundle that has no DOM.
|
|
440
|
+
templateOptions: { compilerOptions },
|
|
441
|
+
})
|
|
442
|
+
: null;
|
|
443
|
+
// Each piece with the map that explains it, so the module can carry ONE map over
|
|
444
|
+
// the whole `.vue` file instead of pointing every stack frame at a generated line.
|
|
445
|
+
const parts = [
|
|
446
|
+
script === null
|
|
447
|
+
? { code: `const ${SFC_BINDING} = {};`, map: null }
|
|
448
|
+
: { code: script.content, map: script.map ?? null },
|
|
449
|
+
];
|
|
450
|
+
if (descriptor.template !== null) {
|
|
451
|
+
const template = compileTemplate({
|
|
452
|
+
id,
|
|
453
|
+
filename,
|
|
454
|
+
source: descriptor.template.content,
|
|
455
|
+
// Safe to reuse ONLY because `parse` above got the parser options.
|
|
456
|
+
ast: descriptor.template.ast,
|
|
457
|
+
compilerOptions: { ...compilerOptions, bindingMetadata: script?.bindings },
|
|
458
|
+
});
|
|
459
|
+
if (template.errors.length > 0) {
|
|
460
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: ${filename} has a template that does not compile:\n ` +
|
|
461
|
+
messagesOf(template.errors));
|
|
462
|
+
}
|
|
463
|
+
// The rename is a same-line replacement, so it moves no mapping to another
|
|
464
|
+
// line — and the `export function render(` line the compiler emits carries no
|
|
465
|
+
// mapping of its own.
|
|
466
|
+
parts.push({ code: renameRenderExport(template.code, filename), map: template.map ?? null });
|
|
467
|
+
parts.push({ code: `${SFC_BINDING}.render = ${RENDER_BINDING};`, map: null });
|
|
468
|
+
}
|
|
469
|
+
parts.push({ code: `${SFC_BINDING}.__file = ${JSON.stringify(filename)};`, map: null });
|
|
470
|
+
parts.push({ code: `export default ${SFC_BINDING};`, map: null });
|
|
471
|
+
return {
|
|
472
|
+
code: parts.map((part) => part.code).join('\n'),
|
|
473
|
+
map: combineSourceMaps(parts.map((part) => ({ lineCount: part.code.split('\n').length, map: part.map }))),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Compile Vue SFCs to a component module for a gjsify custom renderer.
|
|
478
|
+
*
|
|
479
|
+
* Wire it through `package.json#gjsify` so no JS-form config file is needed:
|
|
480
|
+
*
|
|
481
|
+
* ```json
|
|
482
|
+
* "gjsify": { "bundler": { "plugins": [{ "name": "@gjsify/rolldown-plugin-vue" }] } }
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
export function vuePlugin(options = {}) {
|
|
486
|
+
const isCustomElement = options.isCustomElement ?? isGtkHostTag;
|
|
487
|
+
const include = stateless(options.include ?? DEFAULT_INCLUDE);
|
|
488
|
+
const runtimeModuleName = options.runtimeModuleName ?? DEFAULT_RUNTIME_MODULE_NAME;
|
|
489
|
+
return {
|
|
490
|
+
name: 'gjsify-vue',
|
|
491
|
+
resolveId: {
|
|
492
|
+
// Before anything else can claim the specifier — what follows must see
|
|
493
|
+
// the renamed id, not the `.vue` one.
|
|
494
|
+
order: 'pre',
|
|
495
|
+
async handler(source, importer) {
|
|
496
|
+
if (!include.test(source))
|
|
497
|
+
return null;
|
|
498
|
+
// `skipSelf` so the real resolution runs without re-entering here.
|
|
499
|
+
// The result carries the absolute path; the suffix is appended to
|
|
500
|
+
// THAT, so a diagnostic still names the file with the real path as
|
|
501
|
+
// its prefix.
|
|
502
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
503
|
+
if (!resolved || !include.test(resolved.id))
|
|
504
|
+
return null;
|
|
505
|
+
// An EXTERNAL `.vue` is not this plugin's to compile: renaming it would
|
|
506
|
+
// mint a virtual id whose `load` then `readFile`s a path that is not
|
|
507
|
+
// there (ENOENT), for a module the build was told to leave alone. Handed
|
|
508
|
+
// back verbatim instead.
|
|
509
|
+
if (resolved.external)
|
|
510
|
+
return resolved;
|
|
511
|
+
// `moduleSideEffects` and `meta` are the rest of what the real
|
|
512
|
+
// resolution DECIDED — a plugin that answers with a bare string throws
|
|
513
|
+
// away a `sideEffects: false` and every other plugin's `meta` with it.
|
|
514
|
+
return {
|
|
515
|
+
id: `${resolved.id}${VIRTUAL_SUFFIX}`,
|
|
516
|
+
moduleSideEffects: resolved.moduleSideEffects,
|
|
517
|
+
meta: resolved.meta,
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
async load(id) {
|
|
522
|
+
if (!id.endsWith(VIRTUAL_SUFFIX))
|
|
523
|
+
return null;
|
|
524
|
+
const filename = id.slice(0, -VIRTUAL_SUFFIX.length);
|
|
525
|
+
// Re-checked against `include` rather than against a literal `.vue`, so a
|
|
526
|
+
// caller-narrowed filter stays consistent across the two hooks: the pair
|
|
527
|
+
// that mints an id and the pair that reads it must agree, or the id
|
|
528
|
+
// reaches rolldown's file loader and fails on a path that does not exist.
|
|
529
|
+
if (!include.test(filename))
|
|
530
|
+
return null;
|
|
531
|
+
const source = await readFile(filename, 'utf8');
|
|
532
|
+
return compileSfc(source, filename, {
|
|
533
|
+
isCustomElement,
|
|
534
|
+
runtimeModuleName,
|
|
535
|
+
onWarn: (message) => this.warn(message),
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
export default vuePlugin;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fields of a source map this merge READS.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not `source-map-js`'s `RawSourceMap`: that interface types `version` as
|
|
5
|
+
* `string` while every producer writes the number 3, so accepting it would force a cast
|
|
6
|
+
* at the one call site that has a real map.
|
|
7
|
+
*/
|
|
8
|
+
export interface SourceMapChunkMap {
|
|
9
|
+
sources: string[];
|
|
10
|
+
sourcesContent?: string[];
|
|
11
|
+
names: string[];
|
|
12
|
+
mappings: string;
|
|
13
|
+
}
|
|
14
|
+
/** What this module produces — assignable to rolldown's `ExistingRawSourceMap`. */
|
|
15
|
+
export interface CombinedSourceMap {
|
|
16
|
+
version: 3;
|
|
17
|
+
sources: string[];
|
|
18
|
+
sourcesContent: (string | null)[];
|
|
19
|
+
names: string[];
|
|
20
|
+
mappings: string;
|
|
21
|
+
}
|
|
22
|
+
/** One piece of the joined module: how many lines it occupies, and its own map. */
|
|
23
|
+
export interface SourceMapChunk {
|
|
24
|
+
/** Lines this piece occupies in the joined output, including its last. */
|
|
25
|
+
lineCount: number;
|
|
26
|
+
/** `null` for a piece this plugin generated, which no source position explains. */
|
|
27
|
+
map: SourceMapChunkMap | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One map for a module built by joining `chunks` with a newline between each.
|
|
31
|
+
*
|
|
32
|
+
* `null` when no chunk carried a map, so a caller can pass `map: null` — which tells a
|
|
33
|
+
* bundler "this code has no source", not "this code is its own source".
|
|
34
|
+
*/
|
|
35
|
+
export declare function combineSourceMaps(chunks: readonly SourceMapChunk[]): CombinedSourceMap | null;
|
|
36
|
+
/**
|
|
37
|
+
* Where `generatedLine`/`generatedColumn` (both 1-based) came from, or `null`.
|
|
38
|
+
*
|
|
39
|
+
* Exported for the specs: a merged map is only checkable by resolving a position
|
|
40
|
+
* through it, and asserting the `mappings` STRING would pin the encoder's output rather
|
|
41
|
+
* than the mapping it claims to carry.
|
|
42
|
+
*/
|
|
43
|
+
export declare function originalPositionFor(map: CombinedSourceMap, generatedLine: number, generatedColumn: number): {
|
|
44
|
+
source: string;
|
|
45
|
+
line: number;
|
|
46
|
+
column: number;
|
|
47
|
+
} | null;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Joining the source maps `@vue/compiler-sfc` hands back, because the module this
|
|
2
|
+
// plugin emits is a CONCATENATION and neither half's map describes it.
|
|
3
|
+
//
|
|
4
|
+
// `compileScript` maps the script half, `compileTemplate` maps the template half, and
|
|
5
|
+
// both already resolve to the WHOLE `.vue` file: measured on 3.5.41, `sourcesContent[0]`
|
|
6
|
+
// is the SFC source in both, and the template map's source lines already count from the
|
|
7
|
+
// top of the file rather than from the template block. So the source side needs nothing;
|
|
8
|
+
// only the GENERATED side does — where each half landed in the joined module.
|
|
9
|
+
//
|
|
10
|
+
// WHY DECODE AT ALL, rather than concatenate the two `mappings` strings with the right
|
|
11
|
+
// number of `;`. The generated COLUMN resets on every line, but the source index, source
|
|
12
|
+
// line, source column and name index are deltas carried across the entire map. Splice
|
|
13
|
+
// two strings together and the second half's first segment is read relative to the first
|
|
14
|
+
// half's last one, which puts every remaining mapping somewhere else in the file. The
|
|
15
|
+
// Base64-VLQ codec below is what makes offsetting possible; it is spelled out rather than
|
|
16
|
+
// taken as a dependency because `@jridgewell/sourcemap-codec` is only in this tree
|
|
17
|
+
// transitively, and a direct dependency of a tier-1 package costs a lockfile change and a
|
|
18
|
+
// "does it load under GJS" answer for thirty lines.
|
|
19
|
+
/** Base64 digits in `sourceMappingURL` order — index IS the value. */
|
|
20
|
+
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
21
|
+
/** Signed Base64 VLQ: continuation in bit 5, sign in bit 0 of the first digit. */
|
|
22
|
+
function decodeVlq(segment, mappings) {
|
|
23
|
+
const values = [];
|
|
24
|
+
let value = 0;
|
|
25
|
+
let scale = 1;
|
|
26
|
+
for (const char of segment) {
|
|
27
|
+
const digit = BASE64.indexOf(char);
|
|
28
|
+
if (digit === -1)
|
|
29
|
+
throw new Error(malformed(mappings, `${JSON.stringify(char)} is not a Base64 VLQ digit`));
|
|
30
|
+
// Multiplication rather than `<<`: a name index in a large map can exceed the
|
|
31
|
+
// 31 bits a shift keeps, and the overflow would be silent.
|
|
32
|
+
value += (digit & 31) * scale;
|
|
33
|
+
if ((digit & 32) !== 0) {
|
|
34
|
+
scale *= 32;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
values.push(value % 2 === 1 ? -(value - 1) / 2 : value / 2);
|
|
38
|
+
value = 0;
|
|
39
|
+
scale = 1;
|
|
40
|
+
}
|
|
41
|
+
if (scale !== 1)
|
|
42
|
+
throw new Error(malformed(mappings, 'a VLQ value ends mid-continuation'));
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
45
|
+
function encodeVlq(value) {
|
|
46
|
+
let rest = value < 0 ? -value * 2 + 1 : value * 2;
|
|
47
|
+
let encoded = '';
|
|
48
|
+
do {
|
|
49
|
+
const digit = rest % 32;
|
|
50
|
+
rest = Math.floor(rest / 32);
|
|
51
|
+
encoded += BASE64[rest > 0 ? digit + 32 : digit];
|
|
52
|
+
} while (rest > 0);
|
|
53
|
+
return encoded;
|
|
54
|
+
}
|
|
55
|
+
function malformed(mappings, why) {
|
|
56
|
+
return (`@gjsify/rolldown-plugin-vue: cannot read a source map @vue/compiler-sfc produced — ${why}. ` +
|
|
57
|
+
`Mappings began ${JSON.stringify(mappings.slice(0, 40))}.`);
|
|
58
|
+
}
|
|
59
|
+
/** The wire form, line by line, with every position made absolute. */
|
|
60
|
+
function decodeMappings(mappings) {
|
|
61
|
+
let source = 0;
|
|
62
|
+
let sourceLine = 0;
|
|
63
|
+
let sourceColumn = 0;
|
|
64
|
+
let name = 0;
|
|
65
|
+
return mappings.split(';').map((line) => {
|
|
66
|
+
let generatedColumn = 0;
|
|
67
|
+
return line
|
|
68
|
+
.split(',')
|
|
69
|
+
.filter((segment) => segment !== '')
|
|
70
|
+
.map((segment) => {
|
|
71
|
+
const fields = decodeVlq(segment, mappings);
|
|
72
|
+
generatedColumn += fields[0];
|
|
73
|
+
if (fields.length === 1)
|
|
74
|
+
return { generatedColumn };
|
|
75
|
+
// 2 and 3 are not shapes the format has. Refused rather than read as a
|
|
76
|
+
// 4-field segment with zeros, which would point at line 1 column 1.
|
|
77
|
+
if (fields.length < 4) {
|
|
78
|
+
throw new Error(malformed(mappings, `a segment carries ${fields.length} fields, not 1, 4 or 5`));
|
|
79
|
+
}
|
|
80
|
+
source += fields[1];
|
|
81
|
+
sourceLine += fields[2];
|
|
82
|
+
sourceColumn += fields[3];
|
|
83
|
+
const mapping = { generatedColumn, source, sourceLine, sourceColumn };
|
|
84
|
+
if (fields.length > 4) {
|
|
85
|
+
name += fields[4];
|
|
86
|
+
mapping.name = name;
|
|
87
|
+
}
|
|
88
|
+
return mapping;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function encodeMappings(lines) {
|
|
93
|
+
let source = 0;
|
|
94
|
+
let sourceLine = 0;
|
|
95
|
+
let sourceColumn = 0;
|
|
96
|
+
let name = 0;
|
|
97
|
+
return lines
|
|
98
|
+
.map((segments) => {
|
|
99
|
+
let generatedColumn = 0;
|
|
100
|
+
return segments
|
|
101
|
+
.map((mapping) => {
|
|
102
|
+
let encoded = encodeVlq(mapping.generatedColumn - generatedColumn);
|
|
103
|
+
generatedColumn = mapping.generatedColumn;
|
|
104
|
+
if (mapping.source === undefined)
|
|
105
|
+
return encoded;
|
|
106
|
+
encoded +=
|
|
107
|
+
encodeVlq(mapping.source - source) +
|
|
108
|
+
encodeVlq(mapping.sourceLine - sourceLine) +
|
|
109
|
+
encodeVlq(mapping.sourceColumn - sourceColumn);
|
|
110
|
+
source = mapping.source;
|
|
111
|
+
sourceLine = mapping.sourceLine;
|
|
112
|
+
sourceColumn = mapping.sourceColumn;
|
|
113
|
+
if (mapping.name !== undefined) {
|
|
114
|
+
encoded += encodeVlq(mapping.name - name);
|
|
115
|
+
name = mapping.name;
|
|
116
|
+
}
|
|
117
|
+
return encoded;
|
|
118
|
+
})
|
|
119
|
+
.join(',');
|
|
120
|
+
})
|
|
121
|
+
.join(';');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* One map for a module built by joining `chunks` with a newline between each.
|
|
125
|
+
*
|
|
126
|
+
* `null` when no chunk carried a map, so a caller can pass `map: null` — which tells a
|
|
127
|
+
* bundler "this code has no source", not "this code is its own source".
|
|
128
|
+
*/
|
|
129
|
+
export function combineSourceMaps(chunks) {
|
|
130
|
+
if (!chunks.some((chunk) => chunk.map !== null))
|
|
131
|
+
return null;
|
|
132
|
+
const sources = [];
|
|
133
|
+
const sourcesContent = [];
|
|
134
|
+
const names = [];
|
|
135
|
+
const lines = [];
|
|
136
|
+
for (const chunk of chunks) {
|
|
137
|
+
if (chunk.map === null) {
|
|
138
|
+
lines.push(...Array.from({ length: chunk.lineCount }, () => []));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const decoded = decodeMappings(chunk.map.mappings);
|
|
142
|
+
// A map describing MORE lines than its chunk holds would shift every following
|
|
143
|
+
// chunk, and silently: the mappings still decode, they just point into the wrong
|
|
144
|
+
// half of the module. That is the whole failure this function exists to avoid.
|
|
145
|
+
if (decoded.length > chunk.lineCount) {
|
|
146
|
+
throw new Error(`@gjsify/rolldown-plugin-vue: a source map describes ${decoded.length} line(s) for a ` +
|
|
147
|
+
`${chunk.lineCount}-line chunk. Every mapping after it would name the wrong line.`);
|
|
148
|
+
}
|
|
149
|
+
// Source and name indices are per-map; the merged map has one table for all.
|
|
150
|
+
const sourceIndex = chunk.map.sources.map((source, index) => {
|
|
151
|
+
const existing = sources.indexOf(source);
|
|
152
|
+
if (existing !== -1)
|
|
153
|
+
return existing;
|
|
154
|
+
sources.push(source);
|
|
155
|
+
sourcesContent.push(chunk.map?.sourcesContent?.[index] ?? null);
|
|
156
|
+
return sources.length - 1;
|
|
157
|
+
});
|
|
158
|
+
const nameIndex = chunk.map.names.map((entry) => {
|
|
159
|
+
const existing = names.indexOf(entry);
|
|
160
|
+
if (existing !== -1)
|
|
161
|
+
return existing;
|
|
162
|
+
names.push(entry);
|
|
163
|
+
return names.length - 1;
|
|
164
|
+
});
|
|
165
|
+
for (const segments of decoded) {
|
|
166
|
+
lines.push(segments.map((mapping) => mapping.source === undefined
|
|
167
|
+
? mapping
|
|
168
|
+
: {
|
|
169
|
+
...mapping,
|
|
170
|
+
source: sourceIndex[mapping.source],
|
|
171
|
+
...(mapping.name === undefined ? {} : { name: nameIndex[mapping.name] }),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
for (let line = decoded.length; line < chunk.lineCount; line++)
|
|
175
|
+
lines.push([]);
|
|
176
|
+
}
|
|
177
|
+
return { version: 3, sources, sourcesContent, names, mappings: encodeMappings(lines) };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Where `generatedLine`/`generatedColumn` (both 1-based) came from, or `null`.
|
|
181
|
+
*
|
|
182
|
+
* Exported for the specs: a merged map is only checkable by resolving a position
|
|
183
|
+
* through it, and asserting the `mappings` STRING would pin the encoder's output rather
|
|
184
|
+
* than the mapping it claims to carry.
|
|
185
|
+
*/
|
|
186
|
+
export function originalPositionFor(map, generatedLine, generatedColumn) {
|
|
187
|
+
const segments = decodeMappings(map.mappings)[generatedLine - 1];
|
|
188
|
+
if (segments === undefined)
|
|
189
|
+
return null;
|
|
190
|
+
// The last segment at or before the column: a segment covers from its own generated
|
|
191
|
+
// column up to the next one.
|
|
192
|
+
let found;
|
|
193
|
+
for (const mapping of segments) {
|
|
194
|
+
if (mapping.generatedColumn > generatedColumn - 1)
|
|
195
|
+
break;
|
|
196
|
+
if (mapping.source !== undefined)
|
|
197
|
+
found = mapping;
|
|
198
|
+
}
|
|
199
|
+
if (found === undefined)
|
|
200
|
+
return null;
|
|
201
|
+
return {
|
|
202
|
+
source: map.sources[found.source],
|
|
203
|
+
line: found.sourceLine + 1,
|
|
204
|
+
column: found.sourceColumn + 1,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/rolldown-plugin-vue",
|
|
3
|
+
"version": "0.42.0",
|
|
4
|
+
"description": "Vue 3 single-file-component compiler plugin for Rolldown / Rollup / Vite — targeting a gjsify custom renderer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"module": "lib/index.js",
|
|
8
|
+
"types": "lib/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"default": "./lib/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clear": "gjsify clear lib test.node.mjs test.gjs.mjs *.tsbuildinfo",
|
|
20
|
+
"check": "gjsify tsc --noEmit",
|
|
21
|
+
"build": "gjsify tsc -p tsconfig.build.json",
|
|
22
|
+
"build:test": "gjsify run build:test:node && gjsify run build:test:gjs",
|
|
23
|
+
"build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
|
|
24
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
25
|
+
"test": "gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
|
|
26
|
+
"test:node": "node test.node.mjs",
|
|
27
|
+
"test:gjs": "gjsify run test.gjs.mjs"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/gjsify/gjsify.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/rolldown-plugin-vue#readme",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"rolldown",
|
|
39
|
+
"rollup",
|
|
40
|
+
"vite",
|
|
41
|
+
"vue",
|
|
42
|
+
"sfc",
|
|
43
|
+
"gtk",
|
|
44
|
+
"gjs"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@vue/compiler-sfc": "^3.5.41"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"rolldown": "^1.1.4"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"rolldown": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@gjsify/cli": "^0.42.0",
|
|
60
|
+
"@gjsify/unit": "^0.42.0",
|
|
61
|
+
"@types/node": "^25.9.2",
|
|
62
|
+
"rolldown": "^1.1.4"
|
|
63
|
+
},
|
|
64
|
+
"gjsify": {
|
|
65
|
+
"tier": 1
|
|
66
|
+
}
|
|
67
|
+
}
|