@megabudino/stack-utils 1.5.0 → 2.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/README.md +16 -3
- package/dist/proxy/client.d.ts +33 -0
- package/dist/proxy/client.js +101 -0
- package/dist/proxy/define-islands.d.ts +16 -0
- package/dist/proxy/define-islands.js +21 -0
- package/dist/proxy/handle.js +32 -5
- package/dist/proxy/index.d.ts +2 -1
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/islands.d.ts +51 -0
- package/dist/proxy/islands.js +105 -0
- package/dist/proxy/transform-html.d.ts +20 -0
- package/dist/proxy/transform-html.js +80 -0
- package/dist/proxy/types.d.ts +120 -32
- package/dist/proxy/vite-plugin-core.d.ts +57 -0
- package/dist/proxy/vite-plugin-core.js +288 -0
- package/dist/proxy/vite-plugin.d.ts +2 -0
- package/dist/proxy/vite-plugin.js +292 -0
- package/dist/proxy/vite.d.ts +1 -0
- package/dist/proxy/vite.js +1 -0
- package/package.json +9 -1
- package/dist/proxy/dom-actions.d.ts +0 -9
- package/dist/proxy/dom-actions.js +0 -54
package/dist/proxy/types.d.ts
CHANGED
|
@@ -1,39 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
import type { Cheerio, CheerioAPI } from 'cheerio';
|
|
3
|
+
/**
|
|
4
|
+
* Activation mode for a Svelte island.
|
|
5
|
+
*
|
|
6
|
+
* - `serverOnly`: render the component server-side and freeze it as HTML.
|
|
7
|
+
* No client runtime is involved; props can be anything.
|
|
8
|
+
* - `mount`: render server-side for first paint, then mount the component on
|
|
9
|
+
* the client by replacing the SSR markup. Props must be JSON-serializable.
|
|
10
|
+
* - `hydrate`: render server-side and hydrate the existing markup in place
|
|
11
|
+
* on the client. Requires that the server output and client component are
|
|
12
|
+
* the exact same compiled Svelte 5 component and that props match.
|
|
13
|
+
*/
|
|
14
|
+
export type IslandMode = 'serverOnly' | 'mount' | 'hydrate';
|
|
15
|
+
/**
|
|
16
|
+
* A registered Svelte component. Typed permissively so consumers can pass
|
|
17
|
+
* either a Svelte 5 function component or a legacy class component without
|
|
18
|
+
* coupling proxy types to Svelte type internals.
|
|
19
|
+
*
|
|
20
|
+
* Use `import Foo from './Foo.svelte'` and pass `Foo` directly.
|
|
21
|
+
*/
|
|
22
|
+
export type IslandComponent = any;
|
|
23
|
+
/** Reference to a registered island for a `DomAction`. */
|
|
24
|
+
export interface IslandSpec {
|
|
25
|
+
/** Registry key. Must exist in `ProxyConfig.islands.components`. */
|
|
26
|
+
name: string;
|
|
27
|
+
/**
|
|
28
|
+
* Props passed to the component. For `mount` and `hydrate` modes the props
|
|
29
|
+
* are JSON-serialized into the page so the client can rehydrate with the
|
|
30
|
+
* same input, so they must be JSON-safe.
|
|
31
|
+
*/
|
|
32
|
+
props?: Record<string, unknown>;
|
|
33
|
+
/** Activation mode. Defaults to `'serverOnly'`. */
|
|
34
|
+
mode?: IslandMode;
|
|
35
|
+
}
|
|
36
|
+
/** Island registry and client bootstrap configuration. */
|
|
37
|
+
export interface ProxyIslandsConfig {
|
|
38
|
+
/**
|
|
39
|
+
* Registry of components keyed by name. Names referenced by `DomAction.island`
|
|
40
|
+
* must exist in this registry or `createProxyHandle` throws.
|
|
41
|
+
*/
|
|
42
|
+
components: Record<string, IslandComponent>;
|
|
43
|
+
/**
|
|
44
|
+
* URL of a client bootstrap script. When set, the proxy injects
|
|
45
|
+
* `<script type="module" src="${clientEntry}"></script>` at the end of the
|
|
46
|
+
* `<body>` whenever at least one `mount` or `hydrate` island is rendered on
|
|
47
|
+
* the page. Leave undefined to wire the script tag manually with
|
|
48
|
+
* `domActions.appendHtml`.
|
|
22
49
|
*/
|
|
23
|
-
|
|
50
|
+
clientEntry?: string;
|
|
24
51
|
/**
|
|
25
|
-
*
|
|
52
|
+
* Optional stylesheet URLs that should be injected into the document head
|
|
53
|
+
* whenever at least one client island (`mount` or `hydrate`) is rendered.
|
|
26
54
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
55
|
+
* This is mainly useful for advanced/manual setups. When using the
|
|
56
|
+
* `proxyIslands()` Vite plugin, these URLs are auto-generated from the
|
|
57
|
+
* emitted client chunk CSS and should not need to be set by hand.
|
|
29
58
|
*/
|
|
30
|
-
|
|
59
|
+
clientStylesheets?: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Cheerio wrapper around the elements passed to `TransformDomContext.renderIsland`.
|
|
63
|
+
*
|
|
64
|
+
* Loosely typed (`Cheerio<any>`) so consumers can pass whatever shape `$(selector)`
|
|
65
|
+
* returns without coupling to cheerio's element internals. Cheerio infers a
|
|
66
|
+
* narrower element type at the call site for any usage outside of this helper.
|
|
67
|
+
*/
|
|
68
|
+
export type TransformDomTarget = Cheerio<any>;
|
|
69
|
+
/**
|
|
70
|
+
* Context object passed to a `transformDom` hook.
|
|
71
|
+
*
|
|
72
|
+
* Carries the SvelteKit request currently being proxied so the hook can make
|
|
73
|
+
* per-request decisions (e.g., based on URL, route id, cookies) and exposes a
|
|
74
|
+
* `renderIsland` helper that mirrors `domActions[i].island`: it renders a
|
|
75
|
+
* registered Svelte component into the matched elements, tags them so the
|
|
76
|
+
* client runtime can pick them up, and registers head fragments and the
|
|
77
|
+
* client bootstrap exactly once for the document.
|
|
78
|
+
*/
|
|
79
|
+
export interface TransformDomContext {
|
|
80
|
+
/** The SvelteKit request event for the proxied request. */
|
|
81
|
+
event: RequestEvent;
|
|
82
|
+
/** Shortcut for `event.url`. */
|
|
83
|
+
url: URL;
|
|
31
84
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
85
|
+
* Render a registered Svelte island into every element in `target`.
|
|
86
|
+
*
|
|
87
|
+
* Throws when `islands.components` is not configured on `ProxyConfig`,
|
|
88
|
+
* when the island name is unknown, or when props cannot be serialized for
|
|
89
|
+
* `mount` / `hydrate` modes. Server-rendered head fragments are deduped by
|
|
90
|
+
* exact string match and the client bootstrap is injected at most once per
|
|
91
|
+
* document.
|
|
34
92
|
*/
|
|
35
|
-
|
|
93
|
+
renderIsland(target: TransformDomTarget, spec: IslandSpec): Promise<void>;
|
|
36
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Arbitrary asynchronous transformation applied to proxied HTML documents.
|
|
97
|
+
*
|
|
98
|
+
* Runs after the standard proxy URL rewrites and after the legacy `domActions`
|
|
99
|
+
* pass (so it sees the same DOM the actions produced), and before
|
|
100
|
+
* `textReplacements` (which remain the last low-level override).
|
|
101
|
+
*
|
|
102
|
+
* The hook mutates the document via the cheerio root `$`. Returning a value is
|
|
103
|
+
* not supported — mutate `$` in place. The same `$` instance is used by
|
|
104
|
+
* `domActions` and the island bootstrap finalization, so all changes compose
|
|
105
|
+
* into a single rendered document.
|
|
106
|
+
*/
|
|
107
|
+
export type TransformDomHook = ($: CheerioAPI, ctx: TransformDomContext) => void | Promise<void>;
|
|
37
108
|
type DeprecatedProxyOption<Message extends string> = {
|
|
38
109
|
readonly __deprecatedProxyOption: Message;
|
|
39
110
|
};
|
|
@@ -96,9 +167,26 @@ export interface ProxyConfig {
|
|
|
96
167
|
*/
|
|
97
168
|
textReplacements?: Record<string, string>;
|
|
98
169
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
170
|
+
* @deprecated Removed in v2. Express every DOM mutation directly inside
|
|
171
|
+
* `transformDom`, which receives the cheerio root and `ctx.renderIsland`.
|
|
172
|
+
* Passing this field is rejected at startup by `createProxyHandle`.
|
|
173
|
+
*/
|
|
174
|
+
domActions?: DeprecatedProxyOption<'`domActions` was removed in v2. Express mutations with `transformDom` (cheerio root) and call `ctx.renderIsland(...)` for islands.'>;
|
|
175
|
+
/**
|
|
176
|
+
* Arbitrary asynchronous transformation applied to HTML responses.
|
|
177
|
+
*
|
|
178
|
+
* Receives the cheerio root `$` for the proxied document plus a context
|
|
179
|
+
* with the request `event`, the `url`, and a `renderIsland` helper that
|
|
180
|
+
* wires registered Svelte components and the client bootstrap.
|
|
181
|
+
*
|
|
182
|
+
* Runs after the standard URL rewrites and before `textReplacements`.
|
|
183
|
+
* Non-HTML responses are not affected.
|
|
184
|
+
*/
|
|
185
|
+
transformDom?: TransformDomHook;
|
|
186
|
+
/**
|
|
187
|
+
* Svelte component registry and client bootstrap configuration for the
|
|
188
|
+
* islands feature. Required when `transformDom` calls `ctx.renderIsland(...)`.
|
|
101
189
|
*/
|
|
102
|
-
|
|
190
|
+
islands?: ProxyIslandsConfig;
|
|
103
191
|
}
|
|
104
192
|
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface ProxyIslandComponentImport {
|
|
2
|
+
key: string;
|
|
3
|
+
localName: string;
|
|
4
|
+
source: string;
|
|
5
|
+
importKind: 'default' | 'named' | 'namespace';
|
|
6
|
+
importedName?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProxyIslandsDeclaration {
|
|
9
|
+
variableName: string;
|
|
10
|
+
objectLiteralStart: number;
|
|
11
|
+
objectLiteralEnd: number;
|
|
12
|
+
hasClientEntry: boolean;
|
|
13
|
+
hasClientStylesheets: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ExtractedProxyIslandsMetadata {
|
|
16
|
+
declarations: ProxyIslandsDeclaration[];
|
|
17
|
+
components: ProxyIslandComponentImport[];
|
|
18
|
+
}
|
|
19
|
+
export declare function needsAutoWiredAssets(metadata: ExtractedProxyIslandsMetadata): boolean;
|
|
20
|
+
export declare function extractProxyIslandsMetadata(code: string, filePath: string): ExtractedProxyIslandsMetadata | null;
|
|
21
|
+
export declare function buildProxyIslandsClientEntryModule(metadata: ExtractedProxyIslandsMetadata, hooksFilePath: string, generatedFilePath: string): string;
|
|
22
|
+
export declare function injectAutoWiredIslandsConfig(code: string, metadata: ExtractedProxyIslandsMetadata, values: {
|
|
23
|
+
clientEntryExpression: string;
|
|
24
|
+
clientStylesheetsExpression: string;
|
|
25
|
+
}): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build the public URL we inject into proxied HTML for a generated client
|
|
28
|
+
* bundle file or its associated CSS asset.
|
|
29
|
+
*
|
|
30
|
+
* The islands runtime sticks these URLs into the `<head>` (and end of `<body>`)
|
|
31
|
+
* of pages whose pathname has nothing to do with the bundle layout. Relative
|
|
32
|
+
* URLs like `./_app/...` only resolve correctly when the page itself sits at
|
|
33
|
+
* the site root, which makes the assets unreachable as soon as the proxy
|
|
34
|
+
* serves a nested URL. SvelteKit defaults `paths.relative` to true, so Vite's
|
|
35
|
+
* `base` resolves to `./` for the SvelteKit-rendered pages — fine for
|
|
36
|
+
* SvelteKit itself, useless for us. We therefore coerce to an absolute path
|
|
37
|
+
* starting with `/`, and only honour `base` when the consumer explicitly
|
|
38
|
+
* configured one (a base that starts with `/`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function toPublicAssetUrl(fileName: string, base: string | undefined): string;
|
|
41
|
+
/**
|
|
42
|
+
* In dev, `vite-plugin-svelte` serves a `<Component>.svelte?svelte&type=style&lang.css`
|
|
43
|
+
* module as a JS wrapper that imports `__vite__updateStyle` and calls it with
|
|
44
|
+
* the CSS literal:
|
|
45
|
+
*
|
|
46
|
+
* const __vite__css = "<escaped css>"
|
|
47
|
+
* __vite__updateStyle(__vite__id, __vite__css)
|
|
48
|
+
*
|
|
49
|
+
* To stop the islands runtime from triggering a flash, we need to ship a
|
|
50
|
+
* single real stylesheet up front (a `<link rel="stylesheet">` blocks render
|
|
51
|
+
* the way JS-injected `<style>` tags cannot). This pulls the raw CSS string
|
|
52
|
+
* out of that wrapper so the dev middleware can concatenate it across every
|
|
53
|
+
* island module.
|
|
54
|
+
*/
|
|
55
|
+
export declare function extractViteCssFromJs(jsCode: string): string;
|
|
56
|
+
/** Match every module id we want to treat as a stylesheet for dev-time extraction. */
|
|
57
|
+
export declare function isCssLikeModuleId(id: string | null | undefined): boolean;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
export function needsAutoWiredAssets(metadata) {
|
|
4
|
+
return metadata.declarations.some((declaration) => !declaration.hasClientEntry || !declaration.hasClientStylesheets);
|
|
5
|
+
}
|
|
6
|
+
export function extractProxyIslandsMetadata(code, filePath) {
|
|
7
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
|
|
8
|
+
const importsByLocalName = collectImports(sourceFile);
|
|
9
|
+
const declarations = [];
|
|
10
|
+
const components = new Map();
|
|
11
|
+
function visit(node) {
|
|
12
|
+
if (ts.isVariableDeclaration(node) &&
|
|
13
|
+
ts.isIdentifier(node.name) &&
|
|
14
|
+
node.initializer &&
|
|
15
|
+
ts.isCallExpression(node.initializer) &&
|
|
16
|
+
isDefineProxyIslandsCall(node.initializer)) {
|
|
17
|
+
const objectLiteral = node.initializer.arguments[0];
|
|
18
|
+
if (!objectLiteral || !ts.isObjectLiteralExpression(objectLiteral)) {
|
|
19
|
+
throw new Error('[stack-utils proxy] `defineProxyIslands(...)` must receive a direct object literal.');
|
|
20
|
+
}
|
|
21
|
+
const componentsProperty = findObjectProperty(objectLiteral, 'components');
|
|
22
|
+
if (!componentsProperty || !ts.isObjectLiteralExpression(componentsProperty.initializer)) {
|
|
23
|
+
throw new Error('[stack-utils proxy] `defineProxyIslands(...)` requires `components: { ... }` as a direct object literal.');
|
|
24
|
+
}
|
|
25
|
+
declarations.push({
|
|
26
|
+
variableName: node.name.text,
|
|
27
|
+
objectLiteralStart: objectLiteral.getStart(sourceFile),
|
|
28
|
+
objectLiteralEnd: objectLiteral.getEnd(),
|
|
29
|
+
hasClientEntry: Boolean(findObjectProperty(objectLiteral, 'clientEntry')),
|
|
30
|
+
hasClientStylesheets: Boolean(findObjectProperty(objectLiteral, 'clientStylesheets'))
|
|
31
|
+
});
|
|
32
|
+
for (const property of componentsProperty.initializer.properties) {
|
|
33
|
+
if (ts.isSpreadAssignment(property)) {
|
|
34
|
+
throw new Error('[stack-utils proxy] `defineProxyIslands({ components })` does not support spread syntax. List components explicitly.');
|
|
35
|
+
}
|
|
36
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
37
|
+
const localName = property.name.text;
|
|
38
|
+
const binding = importsByLocalName.get(localName);
|
|
39
|
+
if (!binding) {
|
|
40
|
+
throw new Error(`[stack-utils proxy] component "${localName}" must be imported directly in hooks.server.ts to enable auto-wiring.`);
|
|
41
|
+
}
|
|
42
|
+
registerComponent(components, {
|
|
43
|
+
key: localName,
|
|
44
|
+
localName,
|
|
45
|
+
...binding
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ts.isPropertyAssignment(property)) {
|
|
50
|
+
const key = readPropertyName(property.name);
|
|
51
|
+
if (!key) {
|
|
52
|
+
throw new Error('[stack-utils proxy] component keys in `defineProxyIslands({ components })` must be plain identifiers or string literals.');
|
|
53
|
+
}
|
|
54
|
+
if (!ts.isIdentifier(property.initializer)) {
|
|
55
|
+
throw new Error(`[stack-utils proxy] component "${key}" must point directly to an imported identifier.`);
|
|
56
|
+
}
|
|
57
|
+
const localName = property.initializer.text;
|
|
58
|
+
const binding = importsByLocalName.get(localName);
|
|
59
|
+
if (!binding) {
|
|
60
|
+
throw new Error(`[stack-utils proxy] component "${key}" must reference an imported identifier to enable auto-wiring.`);
|
|
61
|
+
}
|
|
62
|
+
registerComponent(components, {
|
|
63
|
+
key,
|
|
64
|
+
localName,
|
|
65
|
+
...binding
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw new Error('[stack-utils proxy] unsupported `components` entry. Use `Foo` or `Alias: Foo`.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
}
|
|
74
|
+
visit(sourceFile);
|
|
75
|
+
if (declarations.length === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
declarations,
|
|
80
|
+
components: [...components.values()]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function buildProxyIslandsClientEntryModule(metadata, hooksFilePath, generatedFilePath) {
|
|
84
|
+
const importLines = [
|
|
85
|
+
`import { mountIslands } from '@megabudino/stack-utils/proxy/client';`,
|
|
86
|
+
...metadata.components.map((component) => buildImportLine(component, hooksFilePath, generatedFilePath))
|
|
87
|
+
];
|
|
88
|
+
const componentEntries = metadata.components.map((component) => component.key === component.localName
|
|
89
|
+
? component.localName
|
|
90
|
+
: `${formatObjectKey(component.key)}: ${component.localName}`);
|
|
91
|
+
return [
|
|
92
|
+
'// Generated by @megabudino/stack-utils/proxy/vite.',
|
|
93
|
+
'// Do not edit by hand.',
|
|
94
|
+
...importLines,
|
|
95
|
+
'',
|
|
96
|
+
'mountIslands({',
|
|
97
|
+
'\tcomponents: {',
|
|
98
|
+
...componentEntries.map((entry) => `\t\t${entry},`),
|
|
99
|
+
'\t}',
|
|
100
|
+
'});',
|
|
101
|
+
''
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
export function injectAutoWiredIslandsConfig(code, metadata, values) {
|
|
105
|
+
let transformed = code;
|
|
106
|
+
for (const declaration of [...metadata.declarations].sort((left, right) => right.objectLiteralStart - left.objectLiteralStart)) {
|
|
107
|
+
const objectLiteralSource = transformed.slice(declaration.objectLiteralStart, declaration.objectLiteralEnd);
|
|
108
|
+
const nextLiteral = appendMissingObjectProperties(objectLiteralSource, [
|
|
109
|
+
...(!declaration.hasClientEntry
|
|
110
|
+
? [`clientEntry: ${values.clientEntryExpression}`]
|
|
111
|
+
: []),
|
|
112
|
+
...(!declaration.hasClientStylesheets
|
|
113
|
+
? [`clientStylesheets: ${values.clientStylesheetsExpression}`]
|
|
114
|
+
: [])
|
|
115
|
+
]);
|
|
116
|
+
transformed =
|
|
117
|
+
transformed.slice(0, declaration.objectLiteralStart) +
|
|
118
|
+
nextLiteral +
|
|
119
|
+
transformed.slice(declaration.objectLiteralEnd);
|
|
120
|
+
}
|
|
121
|
+
return transformed;
|
|
122
|
+
}
|
|
123
|
+
function collectImports(sourceFile) {
|
|
124
|
+
const importsByLocalName = new Map();
|
|
125
|
+
for (const statement of sourceFile.statements) {
|
|
126
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const source = statement.moduleSpecifier;
|
|
130
|
+
if (!ts.isStringLiteral(source)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const sourceText = source.text;
|
|
134
|
+
const clause = statement.importClause;
|
|
135
|
+
if (clause.name) {
|
|
136
|
+
importsByLocalName.set(clause.name.text, {
|
|
137
|
+
source: sourceText,
|
|
138
|
+
importKind: 'default'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const bindings = clause.namedBindings;
|
|
142
|
+
if (!bindings) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
146
|
+
importsByLocalName.set(bindings.name.text, {
|
|
147
|
+
source: sourceText,
|
|
148
|
+
importKind: 'namespace'
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const element of bindings.elements) {
|
|
153
|
+
importsByLocalName.set(element.name.text, {
|
|
154
|
+
source: sourceText,
|
|
155
|
+
importKind: 'named',
|
|
156
|
+
importedName: element.propertyName?.text ?? element.name.text
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return importsByLocalName;
|
|
161
|
+
}
|
|
162
|
+
function isDefineProxyIslandsCall(node) {
|
|
163
|
+
return ts.isIdentifier(node.expression) && node.expression.text === 'defineProxyIslands';
|
|
164
|
+
}
|
|
165
|
+
function findObjectProperty(objectLiteral, name) {
|
|
166
|
+
return objectLiteral.properties.find((property) => ts.isPropertyAssignment(property) && readPropertyName(property.name) === name);
|
|
167
|
+
}
|
|
168
|
+
function readPropertyName(name) {
|
|
169
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
170
|
+
return name.text;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
function registerComponent(components, component) {
|
|
175
|
+
const existing = components.get(component.key);
|
|
176
|
+
if (!existing) {
|
|
177
|
+
components.set(component.key, component);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const sameBinding = existing.localName === component.localName &&
|
|
181
|
+
existing.source === component.source &&
|
|
182
|
+
existing.importKind === component.importKind &&
|
|
183
|
+
existing.importedName === component.importedName;
|
|
184
|
+
if (!sameBinding) {
|
|
185
|
+
throw new Error(`[stack-utils proxy] island registry key "${component.key}" is declared more than once with different imports.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function buildImportLine(component, hooksFilePath, generatedFilePath) {
|
|
189
|
+
const source = resolveComponentImportSource(component.source, hooksFilePath, generatedFilePath);
|
|
190
|
+
if (component.importKind === 'default') {
|
|
191
|
+
return `import ${component.localName} from ${JSON.stringify(source)};`;
|
|
192
|
+
}
|
|
193
|
+
if (component.importKind === 'namespace') {
|
|
194
|
+
return `import * as ${component.localName} from ${JSON.stringify(source)};`;
|
|
195
|
+
}
|
|
196
|
+
if (component.importedName && component.importedName !== component.localName) {
|
|
197
|
+
return `import { ${component.importedName} as ${component.localName} } from ${JSON.stringify(source)};`;
|
|
198
|
+
}
|
|
199
|
+
return `import { ${component.localName} } from ${JSON.stringify(source)};`;
|
|
200
|
+
}
|
|
201
|
+
function resolveComponentImportSource(source, hooksFilePath, generatedFilePath) {
|
|
202
|
+
if (!source.startsWith('.')) {
|
|
203
|
+
return source;
|
|
204
|
+
}
|
|
205
|
+
const resolvedSourcePath = path.resolve(path.dirname(hooksFilePath), source);
|
|
206
|
+
const relativeSourcePath = path.relative(path.dirname(generatedFilePath), resolvedSourcePath);
|
|
207
|
+
const normalized = relativeSourcePath.split(path.sep).join('/');
|
|
208
|
+
return normalized.startsWith('.') ? normalized : `./${normalized}`;
|
|
209
|
+
}
|
|
210
|
+
function appendMissingObjectProperties(objectLiteralSource, properties) {
|
|
211
|
+
if (properties.length === 0) {
|
|
212
|
+
return objectLiteralSource;
|
|
213
|
+
}
|
|
214
|
+
const inner = objectLiteralSource.slice(1, -1);
|
|
215
|
+
const trimmedInner = inner.trim();
|
|
216
|
+
if (trimmedInner.length === 0) {
|
|
217
|
+
return `{\n\t${properties.join(',\n\t')}\n}`;
|
|
218
|
+
}
|
|
219
|
+
const separator = /,\s*$/.test(inner) ? '' : ',';
|
|
220
|
+
return `{${inner}${separator}\n\t${properties.join(',\n\t')}\n}`;
|
|
221
|
+
}
|
|
222
|
+
function formatObjectKey(key) {
|
|
223
|
+
return /^[$A-Z_a-z][$\w]*$/.test(key) ? key : JSON.stringify(key);
|
|
224
|
+
}
|
|
225
|
+
function getScriptKind(filePath) {
|
|
226
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {
|
|
227
|
+
return ts.ScriptKind.JS;
|
|
228
|
+
}
|
|
229
|
+
return ts.ScriptKind.TS;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Build the public URL we inject into proxied HTML for a generated client
|
|
233
|
+
* bundle file or its associated CSS asset.
|
|
234
|
+
*
|
|
235
|
+
* The islands runtime sticks these URLs into the `<head>` (and end of `<body>`)
|
|
236
|
+
* of pages whose pathname has nothing to do with the bundle layout. Relative
|
|
237
|
+
* URLs like `./_app/...` only resolve correctly when the page itself sits at
|
|
238
|
+
* the site root, which makes the assets unreachable as soon as the proxy
|
|
239
|
+
* serves a nested URL. SvelteKit defaults `paths.relative` to true, so Vite's
|
|
240
|
+
* `base` resolves to `./` for the SvelteKit-rendered pages — fine for
|
|
241
|
+
* SvelteKit itself, useless for us. We therefore coerce to an absolute path
|
|
242
|
+
* starting with `/`, and only honour `base` when the consumer explicitly
|
|
243
|
+
* configured one (a base that starts with `/`).
|
|
244
|
+
*/
|
|
245
|
+
export function toPublicAssetUrl(fileName, base) {
|
|
246
|
+
let prefix = '/';
|
|
247
|
+
if (base && base.startsWith('/')) {
|
|
248
|
+
prefix = base.endsWith('/') ? base : `${base}/`;
|
|
249
|
+
}
|
|
250
|
+
return `${prefix}${fileName}`.replace(/\/{2,}/g, '/');
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* In dev, `vite-plugin-svelte` serves a `<Component>.svelte?svelte&type=style&lang.css`
|
|
254
|
+
* module as a JS wrapper that imports `__vite__updateStyle` and calls it with
|
|
255
|
+
* the CSS literal:
|
|
256
|
+
*
|
|
257
|
+
* const __vite__css = "<escaped css>"
|
|
258
|
+
* __vite__updateStyle(__vite__id, __vite__css)
|
|
259
|
+
*
|
|
260
|
+
* To stop the islands runtime from triggering a flash, we need to ship a
|
|
261
|
+
* single real stylesheet up front (a `<link rel="stylesheet">` blocks render
|
|
262
|
+
* the way JS-injected `<style>` tags cannot). This pulls the raw CSS string
|
|
263
|
+
* out of that wrapper so the dev middleware can concatenate it across every
|
|
264
|
+
* island module.
|
|
265
|
+
*/
|
|
266
|
+
export function extractViteCssFromJs(jsCode) {
|
|
267
|
+
const match = jsCode.match(/const\s+__vite__css\s*=\s*("(?:\\.|[^"\\])*")/);
|
|
268
|
+
if (!match)
|
|
269
|
+
return '';
|
|
270
|
+
try {
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
272
|
+
const value = new Function(`return ${match[1]};`)();
|
|
273
|
+
return typeof value === 'string' ? value : '';
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Match every module id we want to treat as a stylesheet for dev-time extraction. */
|
|
280
|
+
export function isCssLikeModuleId(id) {
|
|
281
|
+
if (!id)
|
|
282
|
+
return false;
|
|
283
|
+
if (id.includes('?svelte&type=style'))
|
|
284
|
+
return true;
|
|
285
|
+
if (/\.(css|scss|sass|less|styl|stylus|postcss|pcss)(\?|$)/i.test(id))
|
|
286
|
+
return true;
|
|
287
|
+
return false;
|
|
288
|
+
}
|