@platformatic/astro 3.4.1 → 3.5.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/index.js CHANGED
@@ -1,327 +1,31 @@
1
- import middie from '@fastify/middie'
2
- import fastifyStatic from '@fastify/static'
3
- import {
4
- BaseStackable,
5
- transformConfig as basicTransformConfig,
6
- cleanBasePath,
7
- createServerListener,
8
- ensureTrailingSlash,
9
- errors,
10
- getServerUrl,
11
- importFile,
12
- resolvePackage,
13
- schemaOptions
14
- } from '@platformatic/basic'
15
- import { ConfigManager } from '@platformatic/config'
16
- import fastify from 'fastify'
17
- import { existsSync } from 'node:fs'
18
- import { readFile, writeFile } from 'node:fs/promises'
19
- import { dirname, resolve } from 'node:path'
20
- import { pathToFileURL } from 'node:url'
21
- import { satisfies } from 'semver'
22
- import { packageJson, schema } from './lib/schema.js'
23
-
24
- const supportedVersions = '^4.0.0'
25
-
26
- export class AstroStackable extends BaseStackable {
27
- #astro
28
- #app
29
- #server
30
- #basePath
31
-
32
- constructor (options, root, configManager) {
33
- super('astro', packageJson.version, options, root, configManager)
34
- }
35
-
36
- async init () {
37
- this.#astro = resolve(dirname(resolvePackage(this.root, 'astro')), '../..')
38
- const astroPackage = JSON.parse(await readFile(resolve(this.#astro, 'package.json'), 'utf-8'))
39
-
40
- /* c8 ignore next 3 */
41
- if (!satisfies(astroPackage.version, supportedVersions)) {
42
- throw new errors.UnsupportedVersion('astro', astroPackage.version, supportedVersions)
43
- }
44
- }
45
-
46
- async start ({ listen }) {
47
- // Make this idempotent
48
- if (this.url) {
49
- return this.url
50
- }
51
-
52
- if (this.isProduction) {
53
- await this.#startProduction(listen)
54
- } else {
55
- await this.#startDevelopment(listen)
56
- }
57
- }
58
-
59
- async stop () {
60
- if (this.subprocess) {
61
- return this.stopCommand()
62
- }
63
-
64
- return this.isProduction ? this.#app.close() : this.#app.stop()
65
- }
66
-
67
- async build () {
68
- const config = this.configManager.current
69
- const command = config.application.commands.build
70
- const configFile = config.astro.configFile // Note: Astro expect this to be a relative path to the root
71
- let basePath = config.application?.basePath
72
- ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
73
- : undefined
74
-
75
- if (command) {
76
- return this.buildWithCommand(command, basePath)
77
- }
78
-
79
- await this.init()
80
- const { build } = await importFile(resolve(this.#astro, 'dist/core/index.js'))
81
-
82
- await build({
83
- root: this.root,
84
- base: basePath,
85
- outDir: config.application.outputDirectory,
86
- mode: 'production',
87
- configFile,
88
- logLevel: this.logger.level,
89
- integrations: [
90
- {
91
- name: 'platformatic',
92
- hooks: {
93
- 'astro:config:done': ({ config }) => {
94
- basePath = ensureTrailingSlash(cleanBasePath(config.base))
95
- }
96
- }
97
- }
98
- ]
99
- })
100
-
101
- await writeFile(
102
- resolve(this.root, config.application.outputDirectory, '.platformatic-build.json'),
103
- JSON.stringify({ basePath }),
104
- 'utf-8'
105
- )
106
- }
107
-
108
- /* c8 ignore next 5 */
109
- async getWatchConfig () {
110
- return {
111
- enabled: false
112
- }
113
- }
114
-
115
- getMeta () {
116
- const config = this.subprocessConfig ?? this.#app?.config
117
-
118
- const composer = {
119
- tcp: typeof this.url !== 'undefined',
120
- url: this.url,
121
- prefix: this.basePath ?? config?.base ?? this.#basePath,
122
- wantsAbsoluteUrls: true,
123
- needsRootRedirect: true
124
- }
125
-
126
- return { composer }
127
- }
128
-
129
- // This is only used in non SSR production mode as in other modes a TCP server is started
130
- async inject (injectParams, onInject) {
131
- const res = await this.#app.inject(injectParams, onInject)
132
-
133
- /* c8 ignore next 3 */
134
- if (onInject) {
135
- return
136
- }
137
-
138
- // Since inject might be called from the main thread directly via ITC, let's clean it up
139
- const { statusCode, headers, body, payload, rawPayload } = res
140
- return { statusCode, headers, body, payload, rawPayload }
141
- }
142
-
143
- async #startDevelopment () {
144
- // Make this idempotent
145
- if (this.url) {
146
- return this.url
147
- }
148
-
149
- const config = this.configManager.current
150
- const command = this.configManager.current.application.commands.development
151
-
152
- this.#basePath = config.application?.basePath
153
- ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
154
- : undefined
155
-
156
- this.registerGlobals({
157
- id: this.id,
158
- // Always use URL to avoid serialization problem in Windows
159
- root: pathToFileURL(this.root).toString(),
160
- basePath: this.#basePath,
161
- logLevel: this.logger.level
162
- })
163
-
164
- if (command) {
165
- return this.startWithCommand(command)
166
- }
167
-
168
- // Prepare options
169
- const { hostname, port } = this.serverConfig ?? {}
170
- const configFile = config.astro.configFile // Note: Astro expect this to be a relative path to the root
171
-
172
- const serverOptions = {
173
- host: hostname || '127.0.0.1',
174
- port: port || 0
175
- }
176
-
177
- // Require Astro
178
- const serverPromise = createServerListener(
179
- (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
180
- (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
181
- )
182
- const { dev } = await importFile(resolve(this.#astro, 'dist/core/index.js'))
183
-
184
- // Create the server and listen
185
- this.#app = await dev({
186
- root: this.root,
187
- base: this.#basePath,
188
- mode: 'development',
189
- configFile,
190
- logLevel: this.logger.level,
191
- server: serverOptions,
192
- integrations: [
193
- {
194
- name: 'platformatic',
195
- hooks: {
196
- 'astro:config:setup': ({ config }) => {
197
- this.#basePath = ensureTrailingSlash(cleanBasePath(config.base))
198
-
199
- /*
200
- As Astro generates invalid paths in development mode which ignore the basePath
201
- (see https://github.com/withastro/astro/issues/11445), make sure we provide
202
- the prefix in HMR path.
203
- */
204
- config.vite.server ??= {}
205
- config.vite.server.hmr ??= {}
206
- config.vite.server.hmr.path = `/${this.#basePath}/`.replaceAll(/\/+/g, '/')
207
- }
208
- }
209
- }
210
- ]
211
- })
212
-
213
- this.#server = await serverPromise
214
- this.url = getServerUrl(this.#server)
215
- }
216
-
217
- async #startProduction (listen) {
218
- const config = this.configManager.current
219
- const command = this.configManager.current.application.commands.production
220
- const outputDirectory = config.application.outputDirectory
221
-
222
- this.#basePath = config.application?.basePath
223
- ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
224
- : undefined
225
-
226
- this.registerGlobals({
227
- id: this.id,
228
- // Always use URL to avoid serialization problem in Windows
229
- root: pathToFileURL(this.root).toString(),
230
- basePath: this.#basePath,
231
- logLevel: this.logger.level
232
- })
233
-
234
- if (command) {
235
- return this.startWithCommand(command)
236
- }
237
-
238
- if (this.#app && listen) {
239
- const serverOptions = this.serverConfig
240
- await this.#app.listen({ host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 })
241
- this.url = getServerUrl(this.#app.server)
242
- return this.url
243
- }
244
-
245
- this.#app = fastify({ logger: { level: this.logger.level } })
246
-
247
- const root = resolve(this.root, outputDirectory)
248
- this.verifyOutputDirectory(root)
249
-
250
- const buildInfoPath = resolve(root, '.platformatic-build.json')
251
-
252
- if (!this.#basePath && existsSync(buildInfoPath)) {
253
- try {
254
- const buildInfo = JSON.parse(await readFile(buildInfoPath, 'utf-8'))
255
- this.#basePath = buildInfo.basePath
256
- } catch (e) {
257
- console.log(e)
258
- }
259
- }
260
-
261
- const ssrEntrypoint = resolve(this.root, outputDirectory, 'server/entry.mjs')
262
-
263
- if (existsSync(ssrEntrypoint)) {
264
- const { handler } = await importFile(ssrEntrypoint)
265
-
266
- await this.#app.register(fastifyStatic, {
267
- root: resolve(this.root, outputDirectory, 'client'),
268
- prefix: this.#basePath,
269
- prefixAvoidTrailingSlash: true,
270
- schemaHide: true
271
- })
272
-
273
- await this.#app.register(middie)
274
- await this.#app.use(this.#basePath, handler)
275
- } else {
276
- await this.#app.register(fastifyStatic, {
277
- root,
278
- prefix: this.#basePath,
279
- prefixAvoidTrailingSlash: true,
280
- schemaHide: true
281
- })
282
- }
283
-
284
- await this.#app.ready()
285
- }
1
+ import { transform as basicTransform, resolve, validationOptions } from '@platformatic/basic'
2
+ import { kMetadata, loadConfiguration as utilsLoadConfiguration } from '@platformatic/foundation'
3
+ import { AstroCapability } from './lib/capability.js'
4
+ import { schema } from './lib/schema.js'
5
+
6
+ /* c8 ignore next 5 */
7
+ export async function transform (config, schema, options) {
8
+ config = await basicTransform(config, schema, options)
9
+ config.watch = { enabled: false }
10
+ return config
286
11
  }
287
12
 
288
- /* c8 ignore next 9 */
289
- function transformConfig () {
290
- if (this.current.watch === undefined) {
291
- this.current.watch = { enabled: false }
292
- }
293
-
294
- if (typeof this.current.watch !== 'object') {
295
- this.current.watch = { enabled: this.current.watch || false }
296
- }
13
+ export async function loadConfiguration (configOrRoot, sourceOrConfig, context) {
14
+ const { root, source } = await resolve(configOrRoot, sourceOrConfig, 'application')
297
15
 
298
- basicTransformConfig.call(this)
299
- }
300
-
301
- export async function buildStackable (opts) {
302
- const root = opts.context.directory
303
-
304
- const configManager = new ConfigManager({
305
- schema,
306
- source: opts.config ?? {},
307
- schemaOptions,
308
- transformConfig,
309
- dirname: root
16
+ return utilsLoadConfiguration(source, context?.schema ?? schema, {
17
+ validationOptions,
18
+ transform,
19
+ replaceEnv: true,
20
+ root,
21
+ ...context
310
22
  })
311
- await configManager.parseAndValidate()
312
-
313
- return new AstroStackable(opts, root, configManager)
314
23
  }
315
24
 
316
- export { schema, schemaComponents } from './lib/schema.js'
317
-
318
- export default {
319
- configType: 'astro',
320
- configManagerConfig: {
321
- schemaOptions,
322
- transformConfig
323
- },
324
- buildStackable,
325
- schema,
326
- version: packageJson.version
25
+ export async function create (configOrRoot, sourceOrConfig, context) {
26
+ const config = await loadConfiguration(configOrRoot, sourceOrConfig, context)
27
+ return new AstroCapability(config[kMetadata].root, config, context)
327
28
  }
29
+
30
+ export * from './lib/capability.js'
31
+ export { packageJson, schema, schemaComponents, version } from './lib/schema.js'
@@ -0,0 +1,302 @@
1
+ import middie from '@fastify/middie'
2
+ import fastifyStatic from '@fastify/static'
3
+ import {
4
+ BaseCapability,
5
+ cleanBasePath,
6
+ createServerListener,
7
+ ensureTrailingSlash,
8
+ errors,
9
+ getServerUrl,
10
+ importFile,
11
+ resolvePackage
12
+ } from '@platformatic/basic'
13
+ import { ensureLoggableError, features } from '@platformatic/foundation'
14
+ import fastify from 'fastify'
15
+ import { existsSync } from 'node:fs'
16
+ import { readFile, writeFile } from 'node:fs/promises'
17
+ import { dirname, resolve } from 'node:path'
18
+ import { satisfies } from 'semver'
19
+ import { version } from './schema.js'
20
+
21
+ const supportedVersions = '^4.0.0 || ^5.0.0'
22
+
23
+ export class AstroCapability extends BaseCapability {
24
+ #astro
25
+ #app
26
+ #server
27
+ #basePath
28
+
29
+ constructor (root, config, context) {
30
+ super('astro', version, root, config, context)
31
+ }
32
+
33
+ async init () {
34
+ await super.init()
35
+
36
+ if (this.isProduction) {
37
+ return
38
+ }
39
+
40
+ this.#astro = resolve(dirname(resolvePackage(this.root, 'astro')), '../..')
41
+ const astroPackage = JSON.parse(await readFile(resolve(this.#astro, 'package.json'), 'utf-8'))
42
+
43
+ if (!satisfies(astroPackage.version, supportedVersions)) {
44
+ throw new errors.UnsupportedVersion('astro', astroPackage.version, supportedVersions)
45
+ }
46
+ }
47
+
48
+ async start ({ listen }) {
49
+ // Make this idempotent
50
+ if (this.url) {
51
+ return this.url
52
+ }
53
+
54
+ if (this.isProduction) {
55
+ await this.#startProduction(listen)
56
+ } else {
57
+ await this.#startDevelopment(listen)
58
+ }
59
+
60
+ await this._collectMetrics()
61
+ }
62
+
63
+ async stop () {
64
+ await super.stop()
65
+
66
+ if (this.childManager) {
67
+ return this.stopCommand()
68
+ } else if (!this.#app) {
69
+ return
70
+ }
71
+
72
+ return this.isProduction ? this.#app.close() : this.#app.stop()
73
+ }
74
+
75
+ async build () {
76
+ const config = this.config
77
+ const command = config.application.commands.build
78
+ const configFile = config.astro.configFile // Note: Astro expect this to be a relative path to the root
79
+ let basePath = config.application?.basePath
80
+ ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
81
+ : undefined
82
+
83
+ if (command) {
84
+ return this.buildWithCommand(command, basePath)
85
+ }
86
+
87
+ await this.init()
88
+ const { build } = await importFile(resolve(this.#astro, 'dist/core/index.js'))
89
+
90
+ try {
91
+ globalThis.platformatic.isBuilding = true
92
+
93
+ await build({
94
+ root: this.root,
95
+ base: basePath,
96
+ outDir: config.application.outputDirectory,
97
+ mode: 'production',
98
+ configFile,
99
+ logLevel: this.logger.level,
100
+ integrations: [
101
+ {
102
+ name: 'platformatic',
103
+ hooks: {
104
+ 'astro:config:done': ({ config }) => {
105
+ basePath = ensureTrailingSlash(cleanBasePath(config.base))
106
+ }
107
+ }
108
+ }
109
+ ]
110
+ })
111
+ } finally {
112
+ globalThis.platformatic.isBuilding = false
113
+ }
114
+
115
+ await writeFile(
116
+ resolve(this.root, config.application.outputDirectory, '.platformatic-build.json'),
117
+ JSON.stringify({ basePath }),
118
+ 'utf-8'
119
+ )
120
+ }
121
+
122
+ /* c8 ignore next 5 */
123
+ async getWatchConfig () {
124
+ return {
125
+ enabled: false
126
+ }
127
+ }
128
+
129
+ getMeta () {
130
+ const config = this.subprocessConfig ?? this.#app?.config
131
+
132
+ const gateway = {
133
+ tcp: typeof this.url !== 'undefined',
134
+ url: this.url,
135
+ prefix: this.basePath ?? config?.base ?? this.#basePath,
136
+ wantsAbsoluteUrls: true,
137
+ needsRootTrailingSlash: true,
138
+ needsRefererBasedRedirect: !this.isProduction
139
+ }
140
+
141
+ return { gateway }
142
+ }
143
+
144
+ // This is only used in non SSR production mode as in other modes a TCP server is started
145
+ async inject (injectParams, onInject) {
146
+ const res = await this.#app.inject(injectParams, onInject)
147
+
148
+ /* c8 ignore next 3 */
149
+ if (onInject) {
150
+ return
151
+ }
152
+
153
+ // Since inject might be called from the main thread directly via ITC, let's clean it up
154
+ const { statusCode, headers, body, payload, rawPayload } = res
155
+ return { statusCode, headers, body, payload, rawPayload }
156
+ }
157
+
158
+ async #startDevelopment () {
159
+ // Make this idempotent
160
+ if (this.url) {
161
+ return this.url
162
+ }
163
+
164
+ const config = this.config
165
+ const command = this.config.application.commands.development
166
+
167
+ this.#basePath = config.application?.basePath
168
+ ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
169
+ : undefined
170
+
171
+ this.registerGlobals({ basePath: this.#basePath })
172
+
173
+ if (command) {
174
+ return this.startWithCommand(command)
175
+ }
176
+
177
+ // Prepare options
178
+ const { hostname, port } = this.serverConfig ?? {}
179
+ const configFile = config.astro.configFile // Note: Astro expect this to be a relative path to the root
180
+
181
+ const serverOptions = {
182
+ host: hostname || '127.0.0.1',
183
+ port: port || 0
184
+ }
185
+
186
+ // Require Astro
187
+ const serverPromise = createServerListener(
188
+ (this.isEntrypoint ? serverOptions?.port : undefined) ?? true,
189
+ (this.isEntrypoint ? serverOptions?.hostname : undefined) ?? true
190
+ )
191
+ const { dev } = await importFile(resolve(this.#astro, 'dist/core/index.js'))
192
+
193
+ // Create the server and listen
194
+ this.#app = await dev({
195
+ root: this.root,
196
+ base: this.#basePath,
197
+ mode: 'development',
198
+ configFile,
199
+ logLevel: this.logger.level,
200
+ server: serverOptions,
201
+ vite: {
202
+ server: {
203
+ allowedHosts: ['.plt.local']
204
+ }
205
+ },
206
+ integrations: [
207
+ {
208
+ name: 'platformatic',
209
+ hooks: {
210
+ 'astro:config:setup': ({ config }) => {
211
+ this.#basePath = ensureTrailingSlash(cleanBasePath(config.base))
212
+
213
+ /*
214
+ As Astro generates invalid paths in development mode which ignore the basePath
215
+ (see https://github.com/withastro/astro/issues/11445), make sure we provide
216
+ the prefix in HMR path.
217
+ */
218
+ config.vite.server ??= {}
219
+ config.vite.server.hmr ??= {}
220
+ config.vite.server.hmr.path = `/${this.#basePath}/`.replaceAll(/\/+/g, '/')
221
+ config.vite.server.fs ??= {}
222
+ config.vite.server.fs.strict = false
223
+ }
224
+ }
225
+ }
226
+ ]
227
+ })
228
+
229
+ this.#server = await serverPromise
230
+ this.url = getServerUrl(this.#server)
231
+ }
232
+
233
+ async #startProduction (listen) {
234
+ const config = this.config
235
+ const command = this.config.application.commands.production
236
+ const outputDirectory = config.application.outputDirectory
237
+
238
+ this.#basePath = config.application?.basePath
239
+ ? ensureTrailingSlash(cleanBasePath(config.application?.basePath))
240
+ : undefined
241
+
242
+ this.registerGlobals({ basePath: this.#basePath })
243
+
244
+ if (command) {
245
+ return this.startWithCommand(command)
246
+ }
247
+
248
+ if (this.#app && listen) {
249
+ const serverOptions = this.serverConfig
250
+ const listenOptions = { host: serverOptions?.hostname || '127.0.0.1', port: serverOptions?.port || 0 }
251
+
252
+ if (this.isProduction && features.node.reusePort) {
253
+ listenOptions.reusePort = true
254
+ }
255
+
256
+ await this.#app.listen(listenOptions)
257
+ this.url = getServerUrl(this.#app.server)
258
+ return this.url
259
+ }
260
+
261
+ this.#app = fastify({ loggerInstance: this.logger })
262
+
263
+ const root = resolve(this.root, outputDirectory)
264
+ this.verifyOutputDirectory(root)
265
+
266
+ const buildInfoPath = resolve(root, '.platformatic-build.json')
267
+
268
+ if (!this.#basePath && existsSync(buildInfoPath)) {
269
+ try {
270
+ const buildInfo = JSON.parse(await readFile(buildInfoPath, 'utf-8'))
271
+ this.#basePath = buildInfo.basePath
272
+ } catch (e) {
273
+ globalThis.platformatic.logger.error({ err: ensureLoggableError(e) }, 'Reading build info failed.')
274
+ }
275
+ }
276
+
277
+ const ssrEntrypoint = resolve(this.root, outputDirectory, 'server/entry.mjs')
278
+
279
+ if (existsSync(ssrEntrypoint)) {
280
+ const { handler } = await importFile(ssrEntrypoint)
281
+
282
+ await this.#app.register(fastifyStatic, {
283
+ root: resolve(this.root, outputDirectory, 'client'),
284
+ prefix: this.#basePath,
285
+ prefixAvoidTrailingSlash: true,
286
+ schemaHide: true
287
+ })
288
+
289
+ await this.#app.register(middie)
290
+ await this.#app.use(this.#basePath, handler)
291
+ } else {
292
+ await this.#app.register(fastifyStatic, {
293
+ root,
294
+ prefix: this.#basePath,
295
+ prefixAvoidTrailingSlash: true,
296
+ schemaHide: true
297
+ })
298
+ }
299
+
300
+ await this.#app.ready()
301
+ }
302
+ }
package/lib/schema.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { schemaComponents as basicSchemaComponents } from '@platformatic/basic'
2
- import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
2
+ import { schemaComponents as utilsSchemaComponents } from '@platformatic/foundation'
3
3
  import { readFileSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
4
5
 
5
- export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
6
+ export const packageJson = JSON.parse(readFileSync(resolve(import.meta.dirname, '../package.json'), 'utf8'))
7
+ export const version = packageJson.version
6
8
 
7
9
  export const astro = {
8
10
  type: 'object',
@@ -20,7 +22,7 @@ export const schemaComponents = { astro }
20
22
  export const schema = {
21
23
  $id: `https://schemas.platformatic.dev/@platformatic/astro/${packageJson.version}.json`,
22
24
  $schema: 'http://json-schema.org/draft-07/schema#',
23
- title: 'Platformatic Astro Stackable',
25
+ title: 'Platformatic Astro Config',
24
26
  type: 'object',
25
27
  properties: {
26
28
  $schema: {
@@ -29,7 +31,8 @@ export const schema = {
29
31
  logger: utilsSchemaComponents.logger,
30
32
  server: utilsSchemaComponents.server,
31
33
  watch: basicSchemaComponents.watch,
32
- application: basicSchemaComponents.application,
34
+ application: basicSchemaComponents.buildableApplication,
35
+ runtime: utilsSchemaComponents.wrappedRuntime,
33
36
  astro
34
37
  },
35
38
  additionalProperties: false