coralite-scripts 0.38.6 → 0.40.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/bin/index.js CHANGED
@@ -3,14 +3,11 @@
3
3
  import loadConfig from '../libs/load-config.js'
4
4
  import { Command, Argument } from 'commander'
5
5
  import server from '../libs/server.js'
6
- import colours from 'kleur'
7
6
  import pkg from '../package.json' with { type: 'json' }
8
- import buildStyles from '../libs/build-styles.js'
9
- import { join, relative, dirname } from 'node:path'
10
- import { deleteDirectoryRecursive, copyDirectory, toMS, toTime, displayError, displayWarning, displayInfo } from '../libs/build-utils.js'
11
- import { createCoralite } from 'coralite'
12
- import { mkdir, writeFile } from 'node:fs/promises'
13
- import ora from 'ora'
7
+ import { join } from 'node:path'
8
+ import { mkdir } from 'node:fs/promises'
9
+ import { buildCommand } from '../libs/commands/build.js'
10
+ import { parseAssetMapping, mergeAssets } from '../libs/assets.js'
14
11
 
15
12
  // remove all Node warnings before doing anything else
16
13
  process.removeAllListeners('warning')
@@ -22,7 +19,9 @@ program
22
19
  .description(pkg.description)
23
20
  .version(pkg.version)
24
21
  .addArgument(new Argument('<mode>', 'Run mode: dev (development server) or build (production compilation)').choices(['dev', 'build']).default('dev'))
25
- .option('-v --verbose', 'Enable verbose logging output')
22
+ .option('-v, --verbose', 'Enable verbose logging output')
23
+ .option('-c, --clean', 'Clear the output directory before building')
24
+ .option('-a, --assets <mapping...>', 'Static assets to copy during build. Format: pkg:path:dest or src:dest')
26
25
 
27
26
  program.parse(process.argv)
28
27
  program.on('error', (err) => {
@@ -31,171 +30,31 @@ program.on('error', (err) => {
31
30
 
32
31
  const options = program.opts()
33
32
  const mode = program.args[0]
34
- const config = await loadConfig()
33
+ const config = await loadConfig(process.cwd())
35
34
 
36
35
  if (!config) {
37
36
  process.exit(1)
38
37
  }
39
38
 
39
+ if (options.assets) {
40
+ try {
41
+ const cliAssets = options.assets.map(parseAssetMapping)
42
+ config.assets = mergeAssets(config.assets, cliAssets)
43
+ } catch (err) {
44
+ console.error(`\n Error: ${err.message}\n`)
45
+ process.exit(1)
46
+ }
47
+ }
48
+
40
49
  if (mode === 'dev') {
41
50
  config.output = join(process.cwd(), '.coralite')
42
51
  await mkdir(config.output, { recursive: true })
43
52
 
44
53
  await server(config, options)
45
54
  } else if (mode === 'build') {
46
- const PAD = ' '
47
- const border = '─'.repeat(Math.min(process.stdout.columns, 36) / 2)
48
- const dash = colours.gray(' ─ ')
49
-
50
- if (options.verbose) {
51
- // log the response time and status code
52
- process.stdout.write('\n' + PAD + colours.yellow('Compiling Coralite... \n\n'))
53
- process.stdout.write(border + colours.inverse(` LOGS `) + border + '\n\n')
54
- } else {
55
- process.stdout.write('\n' + PAD + colours.yellow('Compiling Coralite... \n\n'))
56
- }
57
-
58
- // delete old output files
59
- deleteDirectoryRecursive(config.output)
60
-
61
- // start coralite
62
- const coralite = await createCoralite({
63
- components: config.components,
64
- pages: config.pages,
65
- plugins: config.plugins,
66
- assets: config.assets,
67
- externalStyles: config.styles?.input?.map(input => {
68
- const ext = input.split('.').pop()
69
- return '/assets/css/' + input.split('/').pop().replace(`.${ext}`, '.css')
70
- }),
71
- baseURL: config.baseURL,
72
- output: config.output,
73
- mode: 'production',
74
- onError: ({ level, message, error }) => {
75
- if (level === 'ERR') {
76
- displayError(message, error)
77
- } else if (level === 'WARN') {
78
- displayWarning(message)
79
- } else {
80
- displayInfo(message)
81
- }
82
- }
83
- })
84
-
85
- let spinner
86
- let pageCount = 0
87
- let skippedCount = 0
88
-
89
55
  try {
90
- let componentCount = 0
91
-
92
- if (!options.verbose) {
93
- spinner = ora('Building pages...').start()
94
- }
95
-
96
- const updateSpinnerText = () => {
97
- if (skippedCount > 0) {
98
- spinner.text = `Building pages... (${pageCount} completed, ${skippedCount} skipped)`
99
- } else {
100
- spinner.text = `Building pages... (${pageCount} completed)`
101
- }
102
- }
103
-
104
- // compile website
105
- await coralite.build(async (result) => {
106
- if (result.status === 'skipped') {
107
- skippedCount++
108
- if (!options.verbose) {
109
- updateSpinnerText()
110
- }
111
- return
112
- }
113
-
114
- const relativeDir = relative(config.pages, result.path.dirname)
115
- const outDir = join(config.output, relativeDir)
116
- const outFile = join(outDir, result.path.filename)
117
-
118
- await mkdir(outDir, { recursive: true })
119
- await writeFile(outFile, result.content)
120
-
121
- if (options.verbose) {
122
- process.stdout.write(toTime() + toMS(result.duration) + dash + result.path.pathname + '\n')
123
- } else {
124
- pageCount++
125
- updateSpinnerText()
126
- }
127
- })
128
-
129
- // Write ESM script assets generated during the build phase
130
- if (coralite.outputFiles) {
131
- const assetsDir = join(config.output, 'assets', 'js')
132
-
133
- const assetWrites = Object.values(coralite.outputFiles).map(async (file) => {
134
- const outFile = join(assetsDir, file.hashedPath)
135
- await mkdir(dirname(outFile), { recursive: true })
136
- await writeFile(outFile, file.text)
137
- if (options.verbose) {
138
- const dash = colours.gray(' ─ ')
139
- process.stdout.write(toTime() + toMS(0) + dash + outFile + '\n')
140
- }
141
- })
142
-
143
- await Promise.all(assetWrites)
144
- }
145
-
146
- if (!options.verbose) {
147
- if (skippedCount > 0) {
148
- spinner.succeed(`Pages built (${pageCount} completed, ${skippedCount} skipped)`)
149
- } else {
150
- spinner.succeed(`Pages built (${pageCount} completed)`)
151
- }
152
- if (componentCount > 0) {
153
- ora(`Components built (${componentCount} completed)`).succeed()
154
- }
155
- }
156
-
157
- const publicDir = config.public
158
-
159
- if (publicDir) {
160
- if (!options.verbose) {
161
- spinner = ora('Copying public directory...').start()
162
- }
163
- await copyDirectory(publicDir, config.output)
164
- if (!options.verbose) {
165
- spinner.succeed('Public directory copied')
166
- }
167
- }
168
-
169
- if (config.styles && config.styles.input) {
170
- if (!options.verbose) {
171
- spinner = ora('Building styles...').start()
172
- }
173
-
174
- const results = await buildStyles({
175
- input: config.styles.input,
176
- output: join(config.output, 'assets', 'css'),
177
- processors: config.styles.processors,
178
- minify: mode === 'build',
179
- sourcemap: mode !== 'build'
180
- })
181
-
182
- for (let i = 0; i < results.length; i++) {
183
- const result = results[i]
184
-
185
- if (options.verbose) {
186
- process.stdout.write(toTime() + toMS(result.duration) + dash + result.output + '\n')
187
- }
188
- }
189
-
190
- if (!options.verbose) {
191
- spinner.succeed('Styles built')
192
- }
193
- }
194
- } catch (error) {
195
- if (spinner) {
196
- spinner.fail('Build failed')
197
- }
198
- displayError('Build failed', error)
56
+ await buildCommand(config, options)
57
+ } catch {
199
58
  process.exit(1)
200
59
  }
201
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../libs/config.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,sCAzBW,oBAAoB,GAClB,oBAAoB,CAoJhC;0CA9JsC,mBAAmB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../libs/config.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,sCAzBW,oBAAoB,GAClB,oBAAoB,CA2KhC;0CArLsC,mBAAmB"}
package/libs/assets.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @import { CoraliteStaticAsset } from 'coralite'
3
+ */
4
+
5
+ /**
6
+ * Parses an asset mapping string into an asset object.
7
+ *
8
+ * @param {string} mapping - The mapping string (pkg:path:dest or src:dest).
9
+ * @returns {CoraliteStaticAsset} The parsed asset object.
10
+ * @throws {Error} If the mapping format is invalid.
11
+ */
12
+ export function parseAssetMapping (mapping) {
13
+ const parts = mapping.split(':')
14
+
15
+ if (parts.length === 3) {
16
+ // pkg:path:dest
17
+ return {
18
+ pkg: parts[0],
19
+ path: parts[1],
20
+ dest: parts[2]
21
+ }
22
+ } else if (parts.length === 2) {
23
+ // src:dest
24
+ const src = parts[0]
25
+ if (!src.startsWith('.') && !src.startsWith('/') && !src.startsWith('..')) {
26
+ throw new Error(`Invalid asset mapping "${mapping}". Local paths must start with ".", "..", or "/". NPM package mappings require 3 parts (pkg:path:dest).`)
27
+ }
28
+ return {
29
+ src,
30
+ dest: parts[1]
31
+ }
32
+ } else {
33
+ throw new Error(`Invalid asset mapping "${mapping}". Expected format: pkg:path:dest or src:dest`)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Merges a list of CLI assets into an existing assets array, with CLI assets taking precedence on destination collisions.
39
+ *
40
+ * @param {CoraliteStaticAsset[]} baseAssets - The existing assets array.
41
+ * @param {CoraliteStaticAsset[]} cliAssets - The new assets to merge.
42
+ * @returns {CoraliteStaticAsset[]} The merged assets array.
43
+ */
44
+ export function mergeAssets (baseAssets = [], cliAssets = []) {
45
+ const merged = [...baseAssets]
46
+ for (const cliAsset of cliAssets) {
47
+ const index = merged.findIndex(a => a.dest === cliAsset.dest)
48
+ if (index !== -1) {
49
+ merged[index] = cliAsset
50
+ } else {
51
+ merged.push(cliAsset)
52
+ }
53
+ }
54
+ return merged
55
+ }
@@ -0,0 +1,286 @@
1
+ import colours from 'kleur'
2
+ import buildStyles from '../build-styles.js'
3
+ import { join, relative, dirname } from 'node:path'
4
+ import { existsSync, readdirSync, rmdirSync, statSync, unlinkSync } from 'node:fs'
5
+ import { deleteDirectoryRecursive, copyDirectory, toMS, toTime, displayError, displayWarning, displayInfo } from '../build-utils.js'
6
+ import { createCoralite } from 'coralite'
7
+ import { mkdir, writeFile } from 'node:fs/promises'
8
+ import ora from 'ora'
9
+
10
+ /**
11
+ * @param {import('../../types/index.js').CoraliteScriptConfig} config - The configuration object.
12
+ * @param {any} options - The CLI options.
13
+ * @param {any} logger - The logger object.
14
+ */
15
+ export async function buildCommand (config, options, logger = null) {
16
+ const mode = 'build'
17
+ const PAD = ' '
18
+ const border = '─'.repeat(Math.min(process.stdout.columns || 36, 36) / 2)
19
+ const dash = colours.gray(' ─ ')
20
+
21
+ const log = (msg) => {
22
+ if (logger && logger.write) {
23
+ logger.write(msg)
24
+ } else {
25
+ process.stdout.write(msg)
26
+ }
27
+ }
28
+
29
+ const createSpinner = (text) => {
30
+ if (logger && logger.spinner) {
31
+ return logger.spinner(text)
32
+ }
33
+ return ora(text)
34
+ }
35
+
36
+ const internalLogger = {
37
+ info: (msg) => {
38
+ return logger && logger.info ? logger.info(msg) : displayInfo(msg)
39
+ },
40
+ warn: (msg) => {
41
+ return logger && logger.warn ? logger.warn(msg) : displayWarning(msg)
42
+ },
43
+ error: (msg, err) => {
44
+ return logger && logger.error ? logger.error(msg, err) : displayError(msg, err)
45
+ }
46
+ }
47
+
48
+ if (options.verbose) {
49
+ log('\n' + PAD + colours.yellow('Compiling Coralite... \n\n'))
50
+ log(border + colours.inverse(` LOGS `) + border + '\n\n')
51
+ } else {
52
+ log('\n' + PAD + colours.yellow('Compiling Coralite... \n\n'))
53
+ }
54
+
55
+ if (options.clean) {
56
+ deleteDirectoryRecursive(config.output)
57
+ }
58
+
59
+ const validFiles = new Set()
60
+
61
+ const coralite = await createCoralite({
62
+ components: config.components,
63
+ pages: config.pages,
64
+ plugins: config.plugins,
65
+ assets: config.assets,
66
+ externalStyles: config.styles?.input?.map(input => {
67
+ const ext = input.split('.').pop()
68
+ return '/assets/css/' + input.split('/').pop().replace(`.${ext}`, '.css')
69
+ }),
70
+ baseURL: config.baseURL,
71
+ output: config.output,
72
+ mode: 'production',
73
+ onError: ({ level, message, error }) => {
74
+ if (level === 'ERR') {
75
+ internalLogger.error(message, error)
76
+ } else if (level === 'WARN') {
77
+ internalLogger.warn(message)
78
+ } else {
79
+ internalLogger.info(message)
80
+ }
81
+ }
82
+ })
83
+
84
+ let spinner
85
+ let pageCount = 0
86
+ let skippedCount = 0
87
+
88
+ try {
89
+ const componentCount = 0
90
+
91
+ if (!options.verbose) {
92
+ spinner = createSpinner('Building pages...').start()
93
+ }
94
+
95
+ const updateSpinnerText = () => {
96
+ if (skippedCount > 0) {
97
+ spinner.text = `Building pages... (${pageCount} completed, ${skippedCount} skipped)`
98
+ } else {
99
+ spinner.text = `Building pages... (${pageCount} completed)`
100
+ }
101
+ }
102
+
103
+ await coralite.build(async (result) => {
104
+ const relativeDir = relative(config.pages, result.path.dirname)
105
+ const outDir = join(config.output, relativeDir)
106
+ const outFile = join(outDir, result.path.filename)
107
+
108
+ validFiles.add(outFile)
109
+
110
+ if (result.status === 'skipped') {
111
+ skippedCount++
112
+ if (!options.verbose) {
113
+ updateSpinnerText()
114
+ }
115
+ return
116
+ }
117
+
118
+ await mkdir(outDir, { recursive: true })
119
+ await writeFile(outFile, result.content)
120
+
121
+ if (options.verbose) {
122
+ log(toTime() + toMS(result.duration) + dash + result.path.pathname + '\n')
123
+ } else {
124
+ pageCount++
125
+ updateSpinnerText()
126
+ }
127
+ })
128
+
129
+ if (coralite.outputFiles) {
130
+ const assetsJsDir = join(config.output, 'assets', 'js')
131
+ const assetsCssDir = join(config.output, 'assets', 'css')
132
+
133
+ const assetWrites = Object.values(coralite.outputFiles).map(async (file) => {
134
+ const isCSS = (file.path || file.hashedPath)?.endsWith('.css')
135
+ const baseAssetsDir = isCSS ? assetsCssDir : assetsJsDir
136
+ const outFile = join(baseAssetsDir, file.hashedPath)
137
+
138
+ validFiles.add(outFile)
139
+
140
+ await mkdir(dirname(outFile), { recursive: true })
141
+ await writeFile(outFile, file.text)
142
+ if (options.verbose) {
143
+ const dash = colours.gray(' ─ ')
144
+ log(toTime() + toMS(0) + dash + outFile + '\n')
145
+ }
146
+ })
147
+
148
+ await Promise.all(assetWrites)
149
+ }
150
+
151
+ // Track assets from config to prevent them from being deleted as stale files
152
+ if (config.assets) {
153
+ for (const asset of config.assets) {
154
+ const fullDestPath = join(config.output, asset.dest)
155
+ if (existsSync(fullDestPath)) {
156
+ const stat = statSync(fullDestPath)
157
+ if (stat.isDirectory()) {
158
+ const trackFiles = (dir) => {
159
+ const files = readdirSync(dir)
160
+ for (const file of files) {
161
+ const fullPath = join(dir, file)
162
+ if (statSync(fullPath).isDirectory()) {
163
+ trackFiles(fullPath)
164
+ } else {
165
+ validFiles.add(fullPath)
166
+ }
167
+ }
168
+ }
169
+ trackFiles(fullDestPath)
170
+ } else {
171
+ validFiles.add(fullDestPath)
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ if (!options.verbose) {
178
+ if (skippedCount > 0) {
179
+ spinner.succeed(`Pages built (${pageCount} completed, ${skippedCount} skipped)`)
180
+ } else {
181
+ spinner.succeed(`Pages built (${pageCount} completed)`)
182
+ }
183
+ if (componentCount > 0) {
184
+ createSpinner(`Components built (${componentCount} completed)`).succeed()
185
+ }
186
+ }
187
+
188
+ const publicDir = config.public
189
+
190
+ if (publicDir) {
191
+ if (!options.verbose) {
192
+ spinner = createSpinner('Copying public directory...').start()
193
+ }
194
+ await copyDirectory(publicDir, config.output)
195
+
196
+ const trackPublicFiles = (dir, baseDir) => {
197
+ const files = readdirSync(dir)
198
+ for (const file of files) {
199
+ const fullPath = join(dir, file)
200
+ const stat = statSync(fullPath)
201
+
202
+ if (stat.isDirectory()) {
203
+ trackPublicFiles(fullPath, baseDir)
204
+ } else {
205
+ const relativePath = relative(baseDir, fullPath)
206
+ validFiles.add(join(config.output, relativePath))
207
+ }
208
+ }
209
+ }
210
+ trackPublicFiles(publicDir, publicDir)
211
+
212
+ if (!options.verbose) {
213
+ spinner.succeed('Public directory copied')
214
+ }
215
+ }
216
+
217
+ if (config.styles && config.styles.input) {
218
+ if (!options.verbose) {
219
+ spinner = createSpinner('Building styles...').start()
220
+ }
221
+
222
+ const results = await buildStyles({
223
+ input: config.styles.input,
224
+ output: join(config.output, 'assets', 'css'),
225
+ processors: config.styles.processors,
226
+ minify: mode === 'build',
227
+ sourcemap: mode !== 'build'
228
+ })
229
+
230
+ for (let i = 0; i < results.length; i++) {
231
+ const result = results[i]
232
+ validFiles.add(result.output)
233
+
234
+ if (options.verbose) {
235
+ log(toTime() + toMS(result.duration) + dash + result.output + '\n')
236
+ }
237
+ }
238
+
239
+ if (!options.verbose) {
240
+ spinner.succeed('Styles built')
241
+ }
242
+ }
243
+
244
+ if (!options.clean) {
245
+ if (!options.verbose) {
246
+ spinner = createSpinner('Cleaning up stale files...').start()
247
+ }
248
+
249
+ const cleanupStaleFiles = (dir) => {
250
+ if (!existsSync(dir)) {
251
+ return
252
+ }
253
+
254
+ const files = readdirSync(dir)
255
+ for (const file of files) {
256
+ const fullPath = join(dir, file)
257
+ const stat = statSync(fullPath)
258
+
259
+ if (stat.isDirectory()) {
260
+ cleanupStaleFiles(fullPath)
261
+ if (readdirSync(fullPath).length === 0) {
262
+ rmdirSync(fullPath)
263
+ }
264
+ } else if (!validFiles.has(fullPath)) {
265
+ unlinkSync(fullPath)
266
+
267
+ if (options.verbose) {
268
+ log(toTime() + colours.gray('Deleted stale file: ') + fullPath + '\n')
269
+ }
270
+ }
271
+ }
272
+ }
273
+ cleanupStaleFiles(config.output)
274
+
275
+ if (!options.verbose) {
276
+ spinner.succeed('Cleanup complete')
277
+ }
278
+ }
279
+ } catch (error) {
280
+ if (spinner) {
281
+ spinner.fail('Build failed')
282
+ }
283
+ internalLogger.error('Build failed', error)
284
+ throw error
285
+ }
286
+ }
package/libs/config.js CHANGED
@@ -154,6 +154,29 @@ export function defineConfig (options) {
154
154
  if (!Array.isArray(options.assets)) {
155
155
  throw new Error('Configuration "assets" must be an array')
156
156
  }
157
+
158
+ for (const asset of options.assets) {
159
+ if (typeof asset !== 'object' || asset === null) {
160
+ throw new Error('Configuration "assets" items must be objects')
161
+ }
162
+
163
+ if (!asset.dest || typeof asset.dest !== 'string') {
164
+ throw new Error('Configuration "assets" items must have a string "dest" property')
165
+ }
166
+
167
+ if (asset.src) {
168
+ if (typeof asset.src !== 'string') {
169
+ throw new Error('Configuration "assets" items "src" property must be a string')
170
+ }
171
+ } else {
172
+ if (!asset.pkg || typeof asset.pkg !== 'string') {
173
+ throw new Error('Configuration "assets" items must have a string "pkg" property when "src" is not provided')
174
+ }
175
+ if (!asset.path || typeof asset.path !== 'string') {
176
+ throw new Error('Configuration "assets" items must have a string "path" property when "src" is not provided')
177
+ }
178
+ }
179
+ }
157
180
  }
158
181
 
159
182
  return options
@@ -19,9 +19,10 @@ import { defineConfig } from './config.js'
19
19
  *
20
20
  * const config = await loadConfig()
21
21
  * ```
22
+ * @param {string} [cwd=process.cwd()] - The current working directory.
22
23
  */
23
- async function loadConfig () {
24
- const configPath = pathToFileURL(join(process.cwd(), 'coralite.config.js'))
24
+ async function loadConfig (cwd = process.cwd()) {
25
+ const configPath = pathToFileURL(join(cwd, 'coralite.config.js'))
25
26
 
26
27
  try {
27
28
  await access(configPath)
@@ -42,15 +43,10 @@ async function loadConfig () {
42
43
  return null
43
44
  }
44
45
 
45
- try {
46
- return defineConfig(config.default)
47
- } catch (err) {
48
- displayError('Invalid configuration', err.message)
49
- return null
50
- }
46
+ return defineConfig(config.default)
51
47
  } catch (error) {
52
- displayError('Failed to load configuration file', error)
53
- return null
48
+ displayError('Failed to load configuration file', error.message || error)
49
+ throw error
54
50
  }
55
51
  }
56
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coralite-scripts",
3
- "version": "0.38.6",
3
+ "version": "0.40.0",
4
4
  "description": "Configuration and scripts for Create Coralite.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,11 +61,11 @@
61
61
  "portfinder": "^1.0.38",
62
62
  "postcss": "^8.5.15",
63
63
  "sass": "^1.101.0",
64
- "coralite": "0.38.6"
64
+ "coralite": "0.40.0"
65
65
  },
66
66
  "scripts": {
67
- "build": "premove dist && pnpm build-types",
68
- "build-types": "tsc",
69
- "test-unit": "node --test tests/**/*.spec.js"
67
+ "build": "premove dist && pnpm build:types",
68
+ "build:types": "tsc",
69
+ "test:unit": "node --test tests/**/*.spec.js"
70
70
  }
71
71
  }