@prefresh/rolldown 0.0.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/LICENSE +21 -0
- package/README.md +41 -0
- package/index.d.ts +11 -0
- package/package.json +70 -0
- package/src/index.js +243 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Preact Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @prefresh/rolldown
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@prefresh/rolldown)
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @prefresh/rolldown rolldown
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then add it to your Rolldown config:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import prefresh from '@prefresh/rolldown';
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
plugins: [prefresh()],
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This plugin memoizes `createContext()` calls so context identity survives hot updates. It is intended to be used alongside Rolldown's React refresh transform for component refresh handling.
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
### `library`
|
|
26
|
+
|
|
27
|
+
Libraries to detect `createContext` imports from.
|
|
28
|
+
|
|
29
|
+
Default: `['preact', 'react', 'preact/compat']`
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
prefresh({
|
|
33
|
+
library: ['preact', 'preact/compat'],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `enabled`
|
|
38
|
+
|
|
39
|
+
Enable or disable the transform.
|
|
40
|
+
|
|
41
|
+
Default: `true` in development and `false` otherwise.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'rolldown';
|
|
2
|
+
|
|
3
|
+
export interface PrefreshRolldownOptions {
|
|
4
|
+
library?: string[];
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare function prefreshPlugin(options?: PrefreshRolldownOptions): Plugin;
|
|
9
|
+
|
|
10
|
+
export default prefreshPlugin;
|
|
11
|
+
export { prefreshPlugin as prefresh };
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prefresh/rolldown",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A rolldown plugin to preserve Preact context identity during HMR.",
|
|
5
|
+
"$schema": "https://raw.githubusercontent.com/vitejs/vite-plugin-registry/refs/heads/main/data/schema/extended-package-json.schema.json",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"index.d.ts"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/preactjs/prefresh.git",
|
|
24
|
+
"directory": "packages/rolldown"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/preactjs/prefresh/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/preactjs/prefresh/tree/main/packages/rolldown#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"hmr",
|
|
33
|
+
"preact",
|
|
34
|
+
"prefresh",
|
|
35
|
+
"rolldown",
|
|
36
|
+
"rolldown-plugin",
|
|
37
|
+
"vite-plugin"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"oxc-unshadowed-visitor": "^0.0.1",
|
|
41
|
+
"rolldown-string": "^0.3.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"rolldown": "^1.0.0-rc.12"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"rolldown": "^1.0.0-rc.12",
|
|
48
|
+
"vite": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"vite": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"compatiblePackages": {
|
|
56
|
+
"schemaVersion": 1,
|
|
57
|
+
"rollup": {
|
|
58
|
+
"type": "incompatible",
|
|
59
|
+
"reason": "Uses Rolldown-specific APIs"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public",
|
|
64
|
+
"provenance": false
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"lint": "node --check src/index.js",
|
|
68
|
+
"test": "node --test test/index.test.mjs"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { withMagicString } from 'rolldown-string';
|
|
3
|
+
import { Visitor } from 'rolldown/utils';
|
|
4
|
+
import { ScopedVisitor } from 'oxc-unshadowed-visitor';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LIBRARY = ['preact', 'react', 'preact/compat'];
|
|
7
|
+
const SCRIPT_LANG_RE = /\.(c|m)?(t|j)sx?$/;
|
|
8
|
+
const walk = (program, visitor) => new Visitor(visitor).visit(program);
|
|
9
|
+
|
|
10
|
+
function getLang(id) {
|
|
11
|
+
if (/\.(c|m)?tsx$/.test(id)) return 'tsx';
|
|
12
|
+
if (/\.(c|m)?ts$/.test(id)) return 'ts';
|
|
13
|
+
if (/\.(c|m)?jsx$/.test(id)) return 'jsx';
|
|
14
|
+
return 'js';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createFileHash(id) {
|
|
18
|
+
return createHash('sha256').update(id).digest('hex').slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getSimpleParamNames(params) {
|
|
22
|
+
const names = [];
|
|
23
|
+
for (const parameter of params) {
|
|
24
|
+
if (parameter.type === 'Identifier') names.push(parameter.name);
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getObjectPatternKey(property) {
|
|
30
|
+
if (property.computed) return null;
|
|
31
|
+
if (property.key.type === 'Identifier') return property.key.name;
|
|
32
|
+
if (property.key.type === 'Literal') return String(property.key.value);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildContextKey(fileHash, parentKey, count, paramNames) {
|
|
37
|
+
const base = `${fileHash}${parentKey}${count}`;
|
|
38
|
+
if (paramNames.length === 0) return `\`${base}\``;
|
|
39
|
+
|
|
40
|
+
const suffix = paramNames.map(name => `\${${name}}`).join('');
|
|
41
|
+
return `\`${base}_${suffix}\``;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function prefreshPlugin(options = {}) {
|
|
45
|
+
const libraries = new Set(options.library || DEFAULT_LIBRARY);
|
|
46
|
+
let isEnabled = options.enabled;
|
|
47
|
+
const transformWithMagicString = withMagicString(function (s, id, meta) {
|
|
48
|
+
const program = meta?.ast || this.parse(s.original, { lang: getLang(id) });
|
|
49
|
+
const namedImports = new Set();
|
|
50
|
+
const namespaceImports = new Set();
|
|
51
|
+
|
|
52
|
+
for (const node of program.body) {
|
|
53
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
54
|
+
if (!libraries.has(node.source.value)) continue;
|
|
55
|
+
|
|
56
|
+
for (const specifier of node.specifiers) {
|
|
57
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
58
|
+
const importedName =
|
|
59
|
+
specifier.imported.type === 'Identifier'
|
|
60
|
+
? specifier.imported.name
|
|
61
|
+
: specifier.imported.value;
|
|
62
|
+
|
|
63
|
+
if (importedName === 'createContext') {
|
|
64
|
+
namedImports.add(specifier.local.name);
|
|
65
|
+
}
|
|
66
|
+
} else if (
|
|
67
|
+
specifier.type === 'ImportDefaultSpecifier' ||
|
|
68
|
+
specifier.type === 'ImportNamespaceSpecifier'
|
|
69
|
+
) {
|
|
70
|
+
namespaceImports.add(specifier.local.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const trackedNames = [...namedImports, ...namespaceImports];
|
|
76
|
+
if (trackedNames.length === 0) return;
|
|
77
|
+
|
|
78
|
+
const paramNamesStack = [[]];
|
|
79
|
+
const parentKeyStack = [''];
|
|
80
|
+
let objectPatternDepth = 0;
|
|
81
|
+
|
|
82
|
+
const visitor = new ScopedVisitor({
|
|
83
|
+
trackedNames,
|
|
84
|
+
walk,
|
|
85
|
+
visitor: {
|
|
86
|
+
FunctionDeclaration(node) {
|
|
87
|
+
paramNamesStack.push(getSimpleParamNames(node.params));
|
|
88
|
+
},
|
|
89
|
+
'FunctionDeclaration:exit'() {
|
|
90
|
+
paramNamesStack.pop();
|
|
91
|
+
},
|
|
92
|
+
FunctionExpression(node) {
|
|
93
|
+
paramNamesStack.push(getSimpleParamNames(node.params));
|
|
94
|
+
},
|
|
95
|
+
'FunctionExpression:exit'() {
|
|
96
|
+
paramNamesStack.pop();
|
|
97
|
+
},
|
|
98
|
+
ArrowFunctionExpression(node) {
|
|
99
|
+
paramNamesStack.push(getSimpleParamNames(node.params));
|
|
100
|
+
},
|
|
101
|
+
'ArrowFunctionExpression:exit'() {
|
|
102
|
+
paramNamesStack.pop();
|
|
103
|
+
},
|
|
104
|
+
VariableDeclarator(node) {
|
|
105
|
+
if (node.id.type === 'Identifier') {
|
|
106
|
+
parentKeyStack.push(`$${node.id.name}`);
|
|
107
|
+
} else {
|
|
108
|
+
parentKeyStack.push(parentKeyStack[parentKeyStack.length - 1]);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
'VariableDeclarator:exit'() {
|
|
112
|
+
parentKeyStack.pop();
|
|
113
|
+
},
|
|
114
|
+
AssignmentExpression(node) {
|
|
115
|
+
if (node.left.type === 'Identifier') {
|
|
116
|
+
parentKeyStack.push(`_${node.left.name}`);
|
|
117
|
+
} else {
|
|
118
|
+
parentKeyStack.push(parentKeyStack[parentKeyStack.length - 1]);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
'AssignmentExpression:exit'() {
|
|
122
|
+
parentKeyStack.pop();
|
|
123
|
+
},
|
|
124
|
+
ObjectPattern() {
|
|
125
|
+
objectPatternDepth++;
|
|
126
|
+
},
|
|
127
|
+
'ObjectPattern:exit'() {
|
|
128
|
+
objectPatternDepth--;
|
|
129
|
+
},
|
|
130
|
+
Property(node) {
|
|
131
|
+
if (objectPatternDepth > 0) {
|
|
132
|
+
const key = getObjectPatternKey(node);
|
|
133
|
+
if (key) {
|
|
134
|
+
parentKeyStack.push(`__${key}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parentKeyStack.push(parentKeyStack[parentKeyStack.length - 1]);
|
|
140
|
+
},
|
|
141
|
+
'Property:exit'() {
|
|
142
|
+
parentKeyStack.pop();
|
|
143
|
+
},
|
|
144
|
+
CallExpression(node, ctx) {
|
|
145
|
+
const callee = node.callee;
|
|
146
|
+
const parentKey = parentKeyStack[parentKeyStack.length - 1];
|
|
147
|
+
const paramNames = paramNamesStack[paramNamesStack.length - 1];
|
|
148
|
+
|
|
149
|
+
if (callee.type === 'Identifier' && namedImports.has(callee.name)) {
|
|
150
|
+
ctx.record({
|
|
151
|
+
name: callee.name,
|
|
152
|
+
node,
|
|
153
|
+
data: { callNode: node, parentKey, paramNames },
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
callee.type === 'MemberExpression' &&
|
|
160
|
+
callee.object.type === 'Identifier' &&
|
|
161
|
+
namespaceImports.has(callee.object.name)
|
|
162
|
+
) {
|
|
163
|
+
const isCreateContext = callee.computed
|
|
164
|
+
? callee.property.type === 'Literal' &&
|
|
165
|
+
typeof callee.property.value === 'string' &&
|
|
166
|
+
callee.property.value === 'createContext'
|
|
167
|
+
: callee.property.type === 'Identifier' &&
|
|
168
|
+
callee.property.name === 'createContext';
|
|
169
|
+
|
|
170
|
+
if (isCreateContext) {
|
|
171
|
+
ctx.record({
|
|
172
|
+
name: callee.object.name,
|
|
173
|
+
node,
|
|
174
|
+
data: { callNode: node, parentKey, paramNames },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const records = visitor.walk(program);
|
|
183
|
+
if (records.length === 0) return;
|
|
184
|
+
|
|
185
|
+
const counters = new Map();
|
|
186
|
+
const fileHash = createFileHash(id);
|
|
187
|
+
|
|
188
|
+
for (const record of records) {
|
|
189
|
+
const { callNode, parentKey, paramNames } = record.data;
|
|
190
|
+
const counter = (counters.get(parentKey) || 0) + 1;
|
|
191
|
+
counters.set(parentKey, counter);
|
|
192
|
+
|
|
193
|
+
const callee = s.slice(callNode.callee.start, callNode.callee.end);
|
|
194
|
+
const key = buildContextKey(fileHash, parentKey, counter, paramNames);
|
|
195
|
+
const firstArg = callNode.arguments[0];
|
|
196
|
+
|
|
197
|
+
if (firstArg && firstArg.type !== 'SpreadElement') {
|
|
198
|
+
const value = s.slice(firstArg.start, firstArg.end);
|
|
199
|
+
s.update(
|
|
200
|
+
callNode.start,
|
|
201
|
+
callNode.end,
|
|
202
|
+
`Object.assign(${callee}[${key}] || (${callee}[${key}] = ${callee}(${value})), { __: ${value} })`
|
|
203
|
+
);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
s.update(
|
|
208
|
+
callNode.start,
|
|
209
|
+
callNode.end,
|
|
210
|
+
`${callee}[${key}] || (${callee}[${key}] = ${callee}())`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const plugin = {
|
|
216
|
+
name: 'prefresh-rolldown',
|
|
217
|
+
enforce: 'pre',
|
|
218
|
+
configResolved(config) {
|
|
219
|
+
isEnabled ??= !config.isProduction;
|
|
220
|
+
},
|
|
221
|
+
outputOptions() {
|
|
222
|
+
if ('viteVersion' in this.meta) return;
|
|
223
|
+
isEnabled ??= process.env.NODE_ENV === 'development';
|
|
224
|
+
},
|
|
225
|
+
transform: {
|
|
226
|
+
filter: {
|
|
227
|
+
id: SCRIPT_LANG_RE,
|
|
228
|
+
code: {
|
|
229
|
+
include: 'createContext',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
handler(code, id, meta) {
|
|
233
|
+
if (!isEnabled) return;
|
|
234
|
+
|
|
235
|
+
return transformWithMagicString.call(this, code, id, meta);
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return plugin;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const prefresh = prefreshPlugin;
|