@tinacms/astro 0.4.0 → 0.4.1
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/dist/integration.d.ts +7 -0
- package/dist/integration.js +25 -1
- package/package.json +1 -1
- package/src/__tests__/integration.test.ts +106 -2
- package/src/integration.ts +69 -1
package/dist/integration.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
2
|
export interface TinaIntegrationOptions {
|
|
3
3
|
middlewareOrder?: 'pre' | 'post';
|
|
4
|
+
/**
|
|
5
|
+
* Force the Cloudflare Workers (workerd) `import.meta.url` workaround on or
|
|
6
|
+
* off. When omitted, it is applied automatically whenever the
|
|
7
|
+
* `@astrojs/cloudflare` adapter is detected. Set `false` to opt out, or
|
|
8
|
+
* `true` to force it for a custom Cloudflare setup.
|
|
9
|
+
*/
|
|
10
|
+
cloudflareWorkers?: boolean;
|
|
4
11
|
}
|
|
5
12
|
export default function tina(options?: TinaIntegrationOptions): AstroIntegration;
|
package/dist/integration.js
CHANGED
|
@@ -4,9 +4,13 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
var BRIDGE_ROUTE = "/admin/bridge.js";
|
|
7
|
+
var CLOUDFLARE_ADAPTER_NAME = "@astrojs/cloudflare";
|
|
8
|
+
var WORKERD_IMPORT_META_URL = JSON.stringify("file:///worker.mjs");
|
|
9
|
+
var SERVER_ENVIRONMENTS = /* @__PURE__ */ new Set(["ssr", "astro"]);
|
|
7
10
|
function tina(options = {}) {
|
|
8
11
|
const { middlewareOrder = "pre" } = options;
|
|
9
12
|
let clientDir;
|
|
13
|
+
let isCloudflareAdapter = false;
|
|
10
14
|
return {
|
|
11
15
|
name: "@tinacms/astro",
|
|
12
16
|
hooks: {
|
|
@@ -15,10 +19,18 @@ function tina(options = {}) {
|
|
|
15
19
|
entrypoint: "@tinacms/astro/middleware",
|
|
16
20
|
order: middlewareOrder
|
|
17
21
|
});
|
|
18
|
-
updateConfig({
|
|
22
|
+
updateConfig({
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [
|
|
25
|
+
bridgeDevPlugin(),
|
|
26
|
+
cloudflareImportMetaUrlPlugin(() => isCloudflareAdapter)
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
});
|
|
19
30
|
},
|
|
20
31
|
"astro:config:done": ({ config }) => {
|
|
21
32
|
clientDir = config.build.client;
|
|
33
|
+
isCloudflareAdapter = options.cloudflareWorkers ?? config.adapter?.name === CLOUDFLARE_ADAPTER_NAME;
|
|
22
34
|
},
|
|
23
35
|
"astro:build:done": ({ logger }) => {
|
|
24
36
|
if (!clientDir) return;
|
|
@@ -54,6 +66,18 @@ function bridgeDevPlugin() {
|
|
|
54
66
|
}
|
|
55
67
|
};
|
|
56
68
|
}
|
|
69
|
+
function cloudflareImportMetaUrlPlugin(isCloudflare) {
|
|
70
|
+
const define = { "import.meta.url": WORKERD_IMPORT_META_URL };
|
|
71
|
+
return {
|
|
72
|
+
name: "@tinacms/astro:cloudflare-import-meta-url",
|
|
73
|
+
// No `apply`: the define is needed in both the build (ssr) and dev envs.
|
|
74
|
+
configEnvironment(name) {
|
|
75
|
+
if (!isCloudflare()) return;
|
|
76
|
+
if (!SERVER_ENVIRONMENTS.has(name)) return;
|
|
77
|
+
return { define };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
57
81
|
function emitBridgeAsset(adminDir, logger) {
|
|
58
82
|
try {
|
|
59
83
|
mkdirSync(adminDir, { recursive: true });
|
package/package.json
CHANGED
|
@@ -12,11 +12,11 @@ type ConfigSetupArg = Parameters<NonNullable<Hooks['astro:config:setup']>>[0];
|
|
|
12
12
|
type ConfigDoneArg = Parameters<NonNullable<Hooks['astro:config:done']>>[0];
|
|
13
13
|
type BuildDoneArg = Parameters<NonNullable<Hooks['astro:build:done']>>[0];
|
|
14
14
|
|
|
15
|
-
function runConfigSetup() {
|
|
15
|
+
function runConfigSetup(options?: Parameters<typeof tina>[0]) {
|
|
16
16
|
const addMiddleware = vi.fn();
|
|
17
17
|
const updateConfig = vi.fn();
|
|
18
18
|
const logger = { warn: vi.fn(), info: vi.fn() };
|
|
19
|
-
const integration = tina();
|
|
19
|
+
const integration = tina(options);
|
|
20
20
|
(
|
|
21
21
|
integration.hooks['astro:config:setup'] as NonNullable<
|
|
22
22
|
Hooks['astro:config:setup']
|
|
@@ -28,6 +28,34 @@ function runConfigSetup() {
|
|
|
28
28
|
return { integration, addMiddleware, updateConfig, logger, plugins };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Drive the astro:config:done hook with a chosen adapter so the integration can
|
|
32
|
+
// resolve whether the Cloudflare workaround should apply.
|
|
33
|
+
function runConfigDone(integration: AstroIntegration, adapterName?: string) {
|
|
34
|
+
(
|
|
35
|
+
integration.hooks['astro:config:done'] as NonNullable<
|
|
36
|
+
Hooks['astro:config:done']
|
|
37
|
+
>
|
|
38
|
+
)({
|
|
39
|
+
config: {
|
|
40
|
+
build: { client: pathToFileURL('/tmp/tina-client/') },
|
|
41
|
+
adapter: adapterName ? { name: adapterName, hooks: {} } : undefined,
|
|
42
|
+
},
|
|
43
|
+
} as unknown as ConfigDoneArg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cfPlugin = (plugins: VitePlugin[]) =>
|
|
47
|
+
plugins.find(
|
|
48
|
+
(p) => p.name === '@tinacms/astro:cloudflare-import-meta-url'
|
|
49
|
+
) as VitePlugin;
|
|
50
|
+
|
|
51
|
+
// Invoke a plugin's `configEnvironment` hook (a function in our plugin) without
|
|
52
|
+
// a real Vite environment — it only branches on the environment name.
|
|
53
|
+
function runConfigEnvironment(plugin: VitePlugin, name: string) {
|
|
54
|
+
const hook = plugin.configEnvironment as any;
|
|
55
|
+
const fn = typeof hook === 'function' ? hook : hook?.handler;
|
|
56
|
+
return fn?.call({}, name, {}, {});
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
// Drive a Vite plugin's `configureServer` and capture the request handler it
|
|
32
60
|
// registers, so we can exercise it without a real dev server.
|
|
33
61
|
function devHandler(plugin: VitePlugin) {
|
|
@@ -96,6 +124,82 @@ describe('tina() integration — bridge dev plugin', () => {
|
|
|
96
124
|
});
|
|
97
125
|
});
|
|
98
126
|
|
|
127
|
+
describe('tina() integration — cloudflare import.meta.url plugin', () => {
|
|
128
|
+
const EXPECTED = JSON.stringify('file:///worker.mjs');
|
|
129
|
+
|
|
130
|
+
it('injects a valid import.meta.url define for the workerd server envs under the cloudflare adapter', () => {
|
|
131
|
+
const { integration, plugins } = runConfigSetup();
|
|
132
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
133
|
+
const plugin = cfPlugin(plugins);
|
|
134
|
+
expect(plugin).toBeDefined();
|
|
135
|
+
|
|
136
|
+
for (const env of ['ssr', 'astro']) {
|
|
137
|
+
const result = runConfigEnvironment(plugin, env);
|
|
138
|
+
expect(result?.define?.['import.meta.url']).toBe(EXPECTED);
|
|
139
|
+
// The placeholder must itself be a valid absolute URL.
|
|
140
|
+
expect(
|
|
141
|
+
() => new URL(JSON.parse(result.define['import.meta.url']))
|
|
142
|
+
).not.toThrow();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('never injects the define into the client or prerender envs', () => {
|
|
147
|
+
const { integration, plugins } = runConfigSetup();
|
|
148
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
149
|
+
const plugin = cfPlugin(plugins);
|
|
150
|
+
// `client` keeps the real import.meta.url; `prerender` may run in Node.
|
|
151
|
+
expect(runConfigEnvironment(plugin, 'client')).toBeUndefined();
|
|
152
|
+
expect(runConfigEnvironment(plugin, 'prerender')).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('does not inject the define for non-cloudflare adapters', () => {
|
|
156
|
+
for (const adapter of ['@astrojs/node', '@astrojs/vercel']) {
|
|
157
|
+
const { integration, plugins } = runConfigSetup();
|
|
158
|
+
runConfigDone(integration, adapter);
|
|
159
|
+
const plugin = cfPlugin(plugins);
|
|
160
|
+
for (const env of ['ssr', 'prerender', 'astro', 'client']) {
|
|
161
|
+
expect(runConfigEnvironment(plugin, env)).toBeUndefined();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not inject the define when no adapter is configured', () => {
|
|
167
|
+
const { integration, plugins } = runConfigSetup();
|
|
168
|
+
runConfigDone(integration);
|
|
169
|
+
expect(runConfigEnvironment(cfPlugin(plugins), 'ssr')).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('reads the adapter flag lazily, after config:done', () => {
|
|
173
|
+
const { integration, plugins } = runConfigSetup();
|
|
174
|
+
const plugin = cfPlugin(plugins);
|
|
175
|
+
// Before config:done the flag is false, so the plugin is inert.
|
|
176
|
+
expect(runConfigEnvironment(plugin, 'ssr')).toBeUndefined();
|
|
177
|
+
// The same plugin instance picks up the adapter once config:done runs.
|
|
178
|
+
runConfigDone(integration, '@astrojs/cloudflare');
|
|
179
|
+
expect(
|
|
180
|
+
runConfigEnvironment(plugin, 'ssr')?.define?.['import.meta.url']
|
|
181
|
+
).toBe(EXPECTED);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('honours the cloudflareWorkers option override', () => {
|
|
185
|
+
// Force on, even with a Node adapter.
|
|
186
|
+
const forcedOn = runConfigSetup({ cloudflareWorkers: true });
|
|
187
|
+
runConfigDone(forcedOn.integration, '@astrojs/node');
|
|
188
|
+
expect(
|
|
189
|
+
runConfigEnvironment(cfPlugin(forcedOn.plugins), 'ssr')?.define?.[
|
|
190
|
+
'import.meta.url'
|
|
191
|
+
]
|
|
192
|
+
).toBe(EXPECTED);
|
|
193
|
+
|
|
194
|
+
// Force off, even with the Cloudflare adapter.
|
|
195
|
+
const forcedOff = runConfigSetup({ cloudflareWorkers: false });
|
|
196
|
+
runConfigDone(forcedOff.integration, '@astrojs/cloudflare');
|
|
197
|
+
expect(
|
|
198
|
+
runConfigEnvironment(cfPlugin(forcedOff.plugins), 'ssr')
|
|
199
|
+
).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
99
203
|
describe('tina() integration — astro:build:done', () => {
|
|
100
204
|
it('emits bridge.js into the client output dir', () => {
|
|
101
205
|
const clientDir = mkdtempSync(join(tmpdir(), 'tina-client-'));
|
package/src/integration.ts
CHANGED
|
@@ -7,11 +7,32 @@ import type { Plugin as VitePlugin } from 'vite';
|
|
|
7
7
|
|
|
8
8
|
export interface TinaIntegrationOptions {
|
|
9
9
|
middlewareOrder?: 'pre' | 'post';
|
|
10
|
+
/**
|
|
11
|
+
* Force the Cloudflare Workers (workerd) `import.meta.url` workaround on or
|
|
12
|
+
* off. When omitted, it is applied automatically whenever the
|
|
13
|
+
* `@astrojs/cloudflare` adapter is detected. Set `false` to opt out, or
|
|
14
|
+
* `true` to force it for a custom Cloudflare setup.
|
|
15
|
+
*/
|
|
16
|
+
cloudflareWorkers?: boolean;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/** Where the injected bridge bootstrap imports the bridge bundle from. */
|
|
13
20
|
const BRIDGE_ROUTE = '/admin/bridge.js';
|
|
14
21
|
|
|
22
|
+
const CLOUDFLARE_ADAPTER_NAME = '@astrojs/cloudflare';
|
|
23
|
+
/** A valid absolute URL that stands in for `import.meta.url` in server bundles. */
|
|
24
|
+
const WORKERD_IMPORT_META_URL = JSON.stringify('file:///worker.mjs');
|
|
25
|
+
/**
|
|
26
|
+
* The Vite environments that run on workerd under `@astrojs/cloudflare` and
|
|
27
|
+
* therefore need the placeholder: `ssr` (the on-demand server build) and `astro`
|
|
28
|
+
* (the dev SSR runtime). The `client` bundle is excluded so the browser keeps
|
|
29
|
+
* the real `import.meta.url`, and `prerender` is excluded because it runs in
|
|
30
|
+
* real Node when `prerenderEnvironment: 'node'` (faking the URL there would
|
|
31
|
+
* break genuine file resolution) — and the island route is `prerender: false`,
|
|
32
|
+
* so it never renders during prerendering anyway.
|
|
33
|
+
*/
|
|
34
|
+
const SERVER_ENVIRONMENTS = new Set(['ssr', 'astro']);
|
|
35
|
+
|
|
15
36
|
export default function tina(
|
|
16
37
|
options: TinaIntegrationOptions = {}
|
|
17
38
|
): AstroIntegration {
|
|
@@ -19,6 +40,10 @@ export default function tina(
|
|
|
19
40
|
// Resolved client output dir, captured at config:done and consumed at
|
|
20
41
|
// build:done (only then is the final output location known).
|
|
21
42
|
let clientDir: URL | undefined;
|
|
43
|
+
// Whether the Cloudflare adapter is in use, resolved at config:done. The
|
|
44
|
+
// injected Vite plugin reads it lazily (via a thunk) because the plugin is
|
|
45
|
+
// constructed at config:setup, before the adapter is known.
|
|
46
|
+
let isCloudflareAdapter = false;
|
|
22
47
|
|
|
23
48
|
return {
|
|
24
49
|
name: '@tinacms/astro',
|
|
@@ -32,10 +57,23 @@ export default function tina(
|
|
|
32
57
|
// it into the user's source tree — config-time writes churn
|
|
33
58
|
// public/admin on every `astro dev`/`astro build`, break on read-only
|
|
34
59
|
// or sandboxed filesystems, and can race `tinacms build`.
|
|
35
|
-
updateConfig({
|
|
60
|
+
updateConfig({
|
|
61
|
+
vite: {
|
|
62
|
+
plugins: [
|
|
63
|
+
bridgeDevPlugin(),
|
|
64
|
+
cloudflareImportMetaUrlPlugin(() => isCloudflareAdapter),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
36
68
|
},
|
|
37
69
|
'astro:config:done': ({ config }) => {
|
|
38
70
|
clientDir = config.build.client;
|
|
71
|
+
// config:done runs before Vite is created and after every integration
|
|
72
|
+
// (including a late-registered adapter) has resolved, so config.adapter
|
|
73
|
+
// is reliable here.
|
|
74
|
+
isCloudflareAdapter =
|
|
75
|
+
options.cloudflareWorkers ??
|
|
76
|
+
config.adapter?.name === CLOUDFLARE_ADAPTER_NAME;
|
|
39
77
|
},
|
|
40
78
|
'astro:build:done': ({ logger }) => {
|
|
41
79
|
// Build: emit the bridge next to the admin SPA in the *output* tree.
|
|
@@ -82,6 +120,36 @@ function bridgeDevPlugin(): VitePlugin {
|
|
|
82
120
|
};
|
|
83
121
|
}
|
|
84
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Cloudflare Workers (workerd) runs the server bundle in a runtime where the
|
|
125
|
+
* bundled `import.meta.url` is not a valid absolute URL. Astro's experimental
|
|
126
|
+
* Container API — used by the on-demand island route — calls
|
|
127
|
+
* `new URL(import.meta.url)` while building its manifest, which throws
|
|
128
|
+
* "Invalid URL string" and 500s the island render in production. The paths it
|
|
129
|
+
* seeds from that URL are never dereferenced for an in-memory render, so a
|
|
130
|
+
* valid placeholder is harmless. We inject it as a Vite `define`, but only in
|
|
131
|
+
* the server environments (never the client bundle, which needs the real value)
|
|
132
|
+
* and only under the Cloudflare adapter (faking it on a Node server would break
|
|
133
|
+
* real file resolution).
|
|
134
|
+
*
|
|
135
|
+
* TODO: remove once Astro guards the unconditional `new URL(import.meta.url)` in
|
|
136
|
+
* createManifest (tracked upstream in withastro/astro).
|
|
137
|
+
*/
|
|
138
|
+
function cloudflareImportMetaUrlPlugin(
|
|
139
|
+
isCloudflare: () => boolean
|
|
140
|
+
): VitePlugin {
|
|
141
|
+
const define = { 'import.meta.url': WORKERD_IMPORT_META_URL };
|
|
142
|
+
return {
|
|
143
|
+
name: '@tinacms/astro:cloudflare-import-meta-url',
|
|
144
|
+
// No `apply`: the define is needed in both the build (ssr) and dev envs.
|
|
145
|
+
configEnvironment(name) {
|
|
146
|
+
if (!isCloudflare()) return;
|
|
147
|
+
if (!SERVER_ENVIRONMENTS.has(name)) return;
|
|
148
|
+
return { define };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
function emitBridgeAsset(adminDir: string, logger: AstroIntegrationLogger) {
|
|
86
154
|
try {
|
|
87
155
|
mkdirSync(adminDir, { recursive: true });
|