@nosto/nosto-cli 1.0.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/.github/copilot-instructions.md +326 -0
- package/.github/dependabot.yml +9 -0
- package/.github/pull_request_template.md +12 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/release.yml +49 -0
- package/.husky/commit-msg +1 -0
- package/.prettierrc +9 -0
- package/LICENSE +29 -0
- package/README.md +154 -0
- package/commitlint.config.js +4 -0
- package/eslint.config.js +36 -0
- package/package.json +63 -0
- package/src/api/library/fetchLibraryFile.ts +18 -0
- package/src/api/retry.ts +28 -0
- package/src/api/source/fetchSourceFile.ts +33 -0
- package/src/api/source/listSourceFiles.ts +13 -0
- package/src/api/source/putSourceFile.ts +14 -0
- package/src/api/source/schema.ts +10 -0
- package/src/api/utils.ts +52 -0
- package/src/bootstrap.sh +26 -0
- package/src/commander.ts +119 -0
- package/src/config/authConfig.ts +42 -0
- package/src/config/config.ts +109 -0
- package/src/config/envConfig.ts +23 -0
- package/src/config/fileConfig.ts +39 -0
- package/src/config/schema.ts +70 -0
- package/src/config/searchTemplatesConfig.ts +33 -0
- package/src/console/logger.ts +93 -0
- package/src/console/userPrompt.ts +16 -0
- package/src/errors/InvalidLoginResponseError.ts +14 -0
- package/src/errors/MissingConfigurationError.ts +14 -0
- package/src/errors/NostoError.ts +13 -0
- package/src/errors/NotNostoTemplateError.ts +15 -0
- package/src/errors/withErrorHandler.ts +50 -0
- package/src/exports.ts +8 -0
- package/src/filesystem/asserts/assertGitRepo.ts +19 -0
- package/src/filesystem/asserts/assertNostoTemplate.ts +34 -0
- package/src/filesystem/calculateTreeHash.ts +28 -0
- package/src/filesystem/esbuild.ts +37 -0
- package/src/filesystem/esbuildPlugins.ts +72 -0
- package/src/filesystem/filesystem.ts +40 -0
- package/src/filesystem/isIgnored.ts +65 -0
- package/src/filesystem/legacyUtils.ts +10 -0
- package/src/filesystem/loadLibrary.ts +31 -0
- package/src/filesystem/processInBatches.ts +38 -0
- package/src/filesystem/utils/getLoaderScript.ts +28 -0
- package/src/index.ts +3 -0
- package/src/modules/login.ts +87 -0
- package/src/modules/logout.ts +13 -0
- package/src/modules/search-templates/build.ts +61 -0
- package/src/modules/search-templates/dev.ts +50 -0
- package/src/modules/search-templates/pull.ts +89 -0
- package/src/modules/search-templates/push.ts +121 -0
- package/src/modules/setup.ts +96 -0
- package/src/modules/status.ts +71 -0
- package/src/utils/withSafeEnvironment.ts +22 -0
- package/test/api/fetchSourceFile.test.ts +30 -0
- package/test/api/putSourceFile.test.ts +34 -0
- package/test/api/retry.test.ts +102 -0
- package/test/api/utils.test.ts +27 -0
- package/test/commander.test.ts +271 -0
- package/test/config/envConfig.test.ts +62 -0
- package/test/config/fileConfig.test.ts +63 -0
- package/test/config/schema.test.ts +96 -0
- package/test/config/searchTemplatesConfig.test.ts +43 -0
- package/test/console/logger.test.ts +96 -0
- package/test/errors/withErrorHandler.test.ts +64 -0
- package/test/filesystem/filesystem.test.ts +53 -0
- package/test/filesystem/plugins.test.ts +35 -0
- package/test/index.test.ts +15 -0
- package/test/modules/search-templates/build.legacy.test.ts +74 -0
- package/test/modules/search-templates/build.modern.test.ts +33 -0
- package/test/modules/search-templates/dev.legacy.test.ts +75 -0
- package/test/modules/search-templates/dev.modern.test.ts +44 -0
- package/test/modules/search-templates/pull.test.ts +96 -0
- package/test/modules/search-templates/push.test.ts +109 -0
- package/test/modules/setup.test.ts +49 -0
- package/test/modules/status.test.ts +22 -0
- package/test/setup.ts +28 -0
- package/test/utils/generateEndpointMock.ts +60 -0
- package/test/utils/mockCommander.ts +22 -0
- package/test/utils/mockConfig.ts +37 -0
- package/test/utils/mockConsole.ts +65 -0
- package/test/utils/mockFileSystem.ts +52 -0
- package/test/utils/mockServer.ts +76 -0
- package/test/utils/mockStarterManifest.ts +42 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
|
|
3
|
+
export const LogLevel = ["debug", "info", "warn", "error"] as const
|
|
4
|
+
|
|
5
|
+
// Persistent per-repo config (.nosto.json file)
|
|
6
|
+
export const PersistentConfigSchema = z.object({
|
|
7
|
+
merchant: z.string(),
|
|
8
|
+
apiKey: z.string().optional(),
|
|
9
|
+
templatesEnv: z.string().default("main"),
|
|
10
|
+
apiUrl: z.string().default("https://api.nosto.com"),
|
|
11
|
+
libraryUrl: z.string().default("https://d11ffvpvtnmt0d.cloudfront.net/library"),
|
|
12
|
+
logLevel: z.enum(LogLevel).default("info"),
|
|
13
|
+
maxRequests: z.coerce.number().default(15)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Config provided on runtime (CLI options)
|
|
17
|
+
export const RuntimeConfigSchema = z.object({
|
|
18
|
+
projectPath: z.string().default("."),
|
|
19
|
+
dryRun: z.boolean().default(false),
|
|
20
|
+
verbose: z.boolean().default(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Authentication config (~/nosto/.auth.json file)
|
|
24
|
+
export const AuthConfigSchema = z.object({
|
|
25
|
+
user: z.string(),
|
|
26
|
+
token: z.string(),
|
|
27
|
+
expiresAt: z.coerce.date()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Environmental variables, alternative to persistent config
|
|
31
|
+
export const EnvironmentConfigSchema = z.object({
|
|
32
|
+
merchant: z.string().optional(),
|
|
33
|
+
apiKey: z.string().optional(),
|
|
34
|
+
templatesEnv: z.string().optional(),
|
|
35
|
+
apiUrl: z.string().optional(),
|
|
36
|
+
libraryUrl: z.string().optional(),
|
|
37
|
+
logLevel: z.enum(LogLevel).optional(),
|
|
38
|
+
maxRequests: z.coerce.number().optional()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Modern config, can be committed to repo (nosto.config.ts file)
|
|
42
|
+
export const SearchTemplatesConfigSchema = z.object({
|
|
43
|
+
onBuild: z
|
|
44
|
+
.custom<() => Promise<void>>((val): val is () => Promise<void> => typeof val === "function")
|
|
45
|
+
.default(() => async () => {
|
|
46
|
+
throw new Error("onBuild function not implemented")
|
|
47
|
+
}),
|
|
48
|
+
onBuildWatch: z
|
|
49
|
+
.custom<(props: OnStartDevProps) => Promise<void>>(
|
|
50
|
+
(val): val is (props: OnStartDevProps) => Promise<void> => typeof val === "function"
|
|
51
|
+
)
|
|
52
|
+
.default(() => async () => {
|
|
53
|
+
throw new Error("onBuildWatch function not implemented")
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
export type OnStartDevProps = {
|
|
57
|
+
onAfterBuild: () => Promise<void>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type SearchTemplatesMode = "modern" | "legacy" | "unknown"
|
|
61
|
+
export type Config = PersistentConfig &
|
|
62
|
+
RuntimeConfig & { auth: AuthConfig; searchTemplates: { mode: SearchTemplatesMode; data: SearchTemplatesConfig } }
|
|
63
|
+
export type PersistentConfig = z.infer<typeof PersistentConfigSchema>
|
|
64
|
+
export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>
|
|
65
|
+
export type AuthConfig = z.infer<typeof AuthConfigSchema>
|
|
66
|
+
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>
|
|
67
|
+
export type SearchTemplatesConfig = z.infer<typeof SearchTemplatesConfigSchema>
|
|
68
|
+
|
|
69
|
+
export const PartialPersistentConfigSchema = PersistentConfigSchema.partial()
|
|
70
|
+
export type PartialPersistentConfig = z.infer<typeof PartialPersistentConfigSchema>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import z from "zod"
|
|
4
|
+
|
|
5
|
+
import { NostoError } from "#errors/NostoError.ts"
|
|
6
|
+
|
|
7
|
+
import { type SearchTemplatesConfig, SearchTemplatesConfigSchema, type SearchTemplatesMode } from "./schema.ts"
|
|
8
|
+
|
|
9
|
+
export async function parseSearchTemplatesConfigFile({
|
|
10
|
+
projectPath
|
|
11
|
+
}: {
|
|
12
|
+
projectPath: string
|
|
13
|
+
}): Promise<{ mode: SearchTemplatesMode; data: SearchTemplatesConfig }> {
|
|
14
|
+
const configPath = path.resolve(projectPath, "nosto.config.ts")
|
|
15
|
+
|
|
16
|
+
// If config is not present, assume legacy mode
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return {
|
|
19
|
+
mode: "legacy",
|
|
20
|
+
data: SearchTemplatesConfigSchema.parse({})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultExport = await import(configPath).then(module => module.default)
|
|
25
|
+
const parsedScriptObject = SearchTemplatesConfigSchema.strict().safeParse(defaultExport)
|
|
26
|
+
if (!parsedScriptObject.success) {
|
|
27
|
+
throw new NostoError("Invalid nosto.config.ts file: " + z.treeifyError(parsedScriptObject.error))
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
mode: "modern",
|
|
31
|
+
data: parsedScriptObject.data
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
|
|
3
|
+
import { LogLevel } from "#config/schema.ts"
|
|
4
|
+
|
|
5
|
+
const formatTimestamp = (date: Date): string => {
|
|
6
|
+
const pad = (num: number) => num.toString().padStart(2, "0")
|
|
7
|
+
const hours = pad(date.getHours())
|
|
8
|
+
const minutes = pad(date.getMinutes())
|
|
9
|
+
const seconds = pad(date.getSeconds())
|
|
10
|
+
return `${hours}:${minutes}:${seconds}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Presets = {
|
|
14
|
+
debug: () => ({
|
|
15
|
+
color: chalk.gray,
|
|
16
|
+
prefix: chalk.gray("[DEBUG]"),
|
|
17
|
+
logger: console.debug,
|
|
18
|
+
logLevel: 0
|
|
19
|
+
}),
|
|
20
|
+
info: () => ({
|
|
21
|
+
color: chalk.white,
|
|
22
|
+
prefix: chalk.white("[INFO] "),
|
|
23
|
+
logger: console.info,
|
|
24
|
+
logLevel: 1
|
|
25
|
+
}),
|
|
26
|
+
success: () => ({
|
|
27
|
+
color: chalk.green,
|
|
28
|
+
prefix: chalk.green("[SUCCESS] "),
|
|
29
|
+
logger: console.info,
|
|
30
|
+
logLevel: 1
|
|
31
|
+
}),
|
|
32
|
+
warn: () => ({
|
|
33
|
+
color: chalk.yellow,
|
|
34
|
+
prefix: chalk.yellow("[WARN] "),
|
|
35
|
+
logger: console.warn,
|
|
36
|
+
logLevel: 2
|
|
37
|
+
}),
|
|
38
|
+
error: () => ({
|
|
39
|
+
color: chalk.red,
|
|
40
|
+
prefix: chalk.red("[ERROR]"),
|
|
41
|
+
logger: console.error,
|
|
42
|
+
logLevel: 3
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const printToLog = (message: string, presetFunc: (typeof Presets)[keyof typeof Presets], extra?: unknown) => {
|
|
47
|
+
const targetLogLevel = LogLevel.indexOf(Logger.context.logLevel)
|
|
48
|
+
const preset = presetFunc()
|
|
49
|
+
if (targetLogLevel > preset.logLevel) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
const timestamp = chalk.dim(formatTimestamp(new Date()))
|
|
53
|
+
const merchantId = Logger.context.merchantId ? `[${chalk.greenBright(Logger.context.merchantId)}] ` : "| "
|
|
54
|
+
const dryRun = Logger.context.isDryRun ? chalk.dim("(DRY RUN) ") : ""
|
|
55
|
+
preset.logger(`${timestamp} ${dryRun}${merchantId}${preset.color(message)}`)
|
|
56
|
+
if (extra) {
|
|
57
|
+
preset.logger(chalk.dim(extra, null, 2))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const Logger = {
|
|
62
|
+
context: {
|
|
63
|
+
logLevel: LogLevel[1] as (typeof LogLevel)[number],
|
|
64
|
+
merchantId: "",
|
|
65
|
+
isDryRun: false
|
|
66
|
+
},
|
|
67
|
+
raw: (message: string, extra?: unknown) => {
|
|
68
|
+
console.log(message)
|
|
69
|
+
if (extra) {
|
|
70
|
+
console.log(extra)
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
debug: (message: string, extra?: unknown) => {
|
|
75
|
+
printToLog(message, Presets.debug, extra)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
info: (message: string, extra?: unknown) => {
|
|
79
|
+
printToLog(message, Presets.info, extra)
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
success: (message: string, extra?: unknown) => {
|
|
83
|
+
printToLog(message, Presets.success, extra)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
warn: (message: string, extra?: unknown) => {
|
|
87
|
+
printToLog(message, Presets.warn, extra)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
error: (message: string, extra?: unknown) => {
|
|
91
|
+
printToLog(message, Presets.error, extra)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { createInterface } from "readline/promises"
|
|
3
|
+
|
|
4
|
+
export async function promptForConfirmation(message: string, defaultAnswer: "Y" | "N"): Promise<boolean> {
|
|
5
|
+
const rl = createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const ynPrompt = defaultAnswer === "Y" ? " (Y/n): " : " (y/N): "
|
|
11
|
+
|
|
12
|
+
const answer = await rl.question("\n" + message + chalk.gray(ynPrompt))
|
|
13
|
+
rl.close()
|
|
14
|
+
const evaluatedAnswer = answer.length === 0 ? defaultAnswer : answer.toUpperCase()
|
|
15
|
+
return evaluatedAnswer === "Y"
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Logger } from "#console/logger.ts"
|
|
2
|
+
|
|
3
|
+
import { NostoError } from "./NostoError.ts"
|
|
4
|
+
|
|
5
|
+
export class InvalidLoginResponseError extends NostoError {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.name = "InvalidLoginResponseError"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
handle() {
|
|
12
|
+
Logger.error(this.message)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Logger } from "#console/logger.ts"
|
|
2
|
+
|
|
3
|
+
import { NostoError } from "./NostoError.ts"
|
|
4
|
+
|
|
5
|
+
export class MissingConfigurationError extends NostoError {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.name = "MissingConfigurationError"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
handle() {
|
|
12
|
+
Logger.error(this.message)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Logger } from "#console/logger.ts"
|
|
2
|
+
|
|
3
|
+
export class NostoError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.name = "NostoError"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
handle() {
|
|
10
|
+
Logger.error(`Generic Nosto error:`)
|
|
11
|
+
Logger.error(`- ${this.message}`)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger } from "#console/logger.ts"
|
|
2
|
+
|
|
3
|
+
import { NostoError } from "./NostoError.ts"
|
|
4
|
+
|
|
5
|
+
export class NotNostoTemplateError extends NostoError {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message)
|
|
8
|
+
this.name = "NotNostoTemplateError"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
handle() {
|
|
12
|
+
Logger.error(`This doesn't seem to be a Nosto template folder.`)
|
|
13
|
+
Logger.error(`- ${this.message}`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { HTTPError, TimeoutError } from "ky"
|
|
3
|
+
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
import { Logger } from "#console/logger.ts"
|
|
6
|
+
|
|
7
|
+
import { InvalidLoginResponseError } from "./InvalidLoginResponseError.ts"
|
|
8
|
+
import { NostoError } from "./NostoError.ts"
|
|
9
|
+
|
|
10
|
+
export async function withErrorHandler(fn: () => void | Promise<void>): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
await fn()
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error instanceof HTTPError) {
|
|
15
|
+
const config = getCachedConfig()
|
|
16
|
+
Logger.error(`HTTP Request failed:`)
|
|
17
|
+
Logger.error(`- ${error.response.status} ${error.response.statusText}`)
|
|
18
|
+
Logger.error(`- ${error.request.method} ${error.request.url}`)
|
|
19
|
+
if (config.verbose) {
|
|
20
|
+
Logger.raw(chalk.red(prettyPrintStack(error.stack)))
|
|
21
|
+
}
|
|
22
|
+
if (!config.verbose) {
|
|
23
|
+
Logger.info(chalk.gray("Rerun with --verbose to see details"))
|
|
24
|
+
}
|
|
25
|
+
} else if (error instanceof TimeoutError) {
|
|
26
|
+
const config = getCachedConfig()
|
|
27
|
+
Logger.error(`HTTP Request timed out:`)
|
|
28
|
+
Logger.error(`- Server did not respond after 10 seconds`)
|
|
29
|
+
Logger.error(`- ${error.request.method} ${error.request.url}`)
|
|
30
|
+
if (config.verbose) {
|
|
31
|
+
Logger.raw(chalk.red(prettyPrintStack(error.stack)))
|
|
32
|
+
}
|
|
33
|
+
} else if (error instanceof InvalidLoginResponseError) {
|
|
34
|
+
Logger.error(`Received malformed login response from server. This is probably a bug on our side.`, error)
|
|
35
|
+
} else if (error instanceof NostoError) {
|
|
36
|
+
error.handle()
|
|
37
|
+
} else {
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function prettyPrintStack(stack: string | undefined) {
|
|
44
|
+
if (!stack) {
|
|
45
|
+
return ""
|
|
46
|
+
}
|
|
47
|
+
const lines = stack.split("\n")
|
|
48
|
+
const filteredLines = lines.filter(line => line.includes(".ts"))
|
|
49
|
+
return filteredLines.join("\n")
|
|
50
|
+
}
|
package/src/exports.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
6
|
+
import { Logger } from "#console/logger.ts"
|
|
7
|
+
|
|
8
|
+
export function assertGitRepo() {
|
|
9
|
+
const { projectPath } = getCachedConfig()
|
|
10
|
+
|
|
11
|
+
if (fs.existsSync(path.join(projectPath, ".git"))) {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const gitCommand = projectPath === "." ? "git init" : `cd ${projectPath} && git init`
|
|
16
|
+
Logger.warn(
|
|
17
|
+
`We heavily recommend using git for your projects. You can start with just running ${chalk.blueBright(gitCommand)}`
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
6
|
+
import { Logger } from "#console/logger.ts"
|
|
7
|
+
import { NotNostoTemplateError } from "#errors/NotNostoTemplateError.ts"
|
|
8
|
+
import { isModernTemplateProject } from "#filesystem/legacyUtils.ts"
|
|
9
|
+
|
|
10
|
+
export function assertNostoTemplate() {
|
|
11
|
+
const { projectPath } = getCachedConfig()
|
|
12
|
+
|
|
13
|
+
Logger.debug("Sanity checking the target directory...")
|
|
14
|
+
const targetFolder = path.resolve(projectPath)
|
|
15
|
+
if (!fs.existsSync(targetFolder)) {
|
|
16
|
+
throw new Error(`Target folder does not exist: ${chalk.cyan(targetFolder)}`)
|
|
17
|
+
}
|
|
18
|
+
if (!fs.statSync(targetFolder).isDirectory()) {
|
|
19
|
+
throw new Error(`Target path is not a directory: ${chalk.cyan(targetFolder)}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isModernTemplateProject()) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const indexFilePath = path.join(projectPath, "index.js")
|
|
27
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
28
|
+
throw new NotNostoTemplateError(`Index file does not exist: ${indexFilePath}`)
|
|
29
|
+
}
|
|
30
|
+
const indexFileContent = fs.readFileSync(indexFilePath, "utf-8")
|
|
31
|
+
if (!indexFileContent.includes("@nosto/preact")) {
|
|
32
|
+
throw new NotNostoTemplateError(`Index file does not contain @nosto/preact: ${indexFilePath}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
|
|
6
|
+
import { getIgnoreInstance } from "./isIgnored.ts"
|
|
7
|
+
|
|
8
|
+
export function calculateTreeHash() {
|
|
9
|
+
const hash = crypto.createHash("sha256")
|
|
10
|
+
for (const filePath of listAllFilesForHashing()) {
|
|
11
|
+
const fileContent = fs.readFileSync(filePath, "utf-8")
|
|
12
|
+
hash.update(fileContent)
|
|
13
|
+
}
|
|
14
|
+
return hash.digest("hex")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function listAllFilesForHashing() {
|
|
18
|
+
const { projectPath } = getCachedConfig()
|
|
19
|
+
const allFiles = fs.readdirSync(projectPath, { withFileTypes: true, recursive: true })
|
|
20
|
+
const ignoreInstance = getIgnoreInstance()
|
|
21
|
+
const filteredFiles = allFiles
|
|
22
|
+
.filter(dirent => !ignoreInstance.isIgnored(dirent))
|
|
23
|
+
.map(dirent => dirent.parentPath + "/" + dirent.name)
|
|
24
|
+
// To relative path
|
|
25
|
+
.map(file => file.replace(projectPath + "/", ""))
|
|
26
|
+
.filter(file => file !== "build/hash")
|
|
27
|
+
return filteredFiles
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { context, type Plugin } from "esbuild"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
|
|
6
|
+
import { createLoaderPlugin, notifyOnRebuildPlugin } from "./esbuildPlugins.ts"
|
|
7
|
+
|
|
8
|
+
export type EsbuildContextOptions = {
|
|
9
|
+
plugins?: Plugin[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getBuildContext(options: EsbuildContextOptions = {}) {
|
|
13
|
+
const { plugins = [] } = options
|
|
14
|
+
const { projectPath } = getCachedConfig()
|
|
15
|
+
return context({
|
|
16
|
+
bundle: true,
|
|
17
|
+
minify: true,
|
|
18
|
+
write: true,
|
|
19
|
+
nodePaths: [path.resolve(import.meta.dirname, "../../node_modules")],
|
|
20
|
+
sourcemap: "linked",
|
|
21
|
+
treeShaking: true,
|
|
22
|
+
jsx: "automatic",
|
|
23
|
+
jsxImportSource: "preact",
|
|
24
|
+
jsxFragment: "Fragment",
|
|
25
|
+
metafile: true,
|
|
26
|
+
target: ["es2018"],
|
|
27
|
+
supported: {
|
|
28
|
+
nesting: false // transpile nested CSS to flat
|
|
29
|
+
},
|
|
30
|
+
outfile: path.resolve(projectPath, "build/bundle.js"),
|
|
31
|
+
entryPoints: [path.resolve(projectPath, "index.js")],
|
|
32
|
+
alias: {
|
|
33
|
+
"@nosto/preact": path.resolve(projectPath, ".nostocache/library/nosto.module.js")
|
|
34
|
+
},
|
|
35
|
+
plugins: [createLoaderPlugin(), notifyOnRebuildPlugin(), ...plugins]
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import * as esbuild from "esbuild"
|
|
5
|
+
import fs from "fs"
|
|
6
|
+
|
|
7
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
8
|
+
import { Logger } from "#console/logger.ts"
|
|
9
|
+
import { getLoaderScript } from "#filesystem/utils/getLoaderScript.ts"
|
|
10
|
+
import { pushSearchTemplate } from "#modules/search-templates/push.ts"
|
|
11
|
+
|
|
12
|
+
export function createLoaderPlugin(): esbuild.Plugin {
|
|
13
|
+
return {
|
|
14
|
+
name: "create-loader",
|
|
15
|
+
setup(build) {
|
|
16
|
+
build.onEnd(result => {
|
|
17
|
+
if (result.errors.length) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
Logger.debug("Generating loader script...")
|
|
21
|
+
const { projectPath } = getCachedConfig()
|
|
22
|
+
const outUri = path.join(projectPath, "build")
|
|
23
|
+
fs.writeFileSync(path.join(outUri, "loader.js"), getLoaderScript())
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* By default, esbuild is silent. This plugin prints a log message with some rebuild stats.
|
|
31
|
+
*/
|
|
32
|
+
const performanceStats = {
|
|
33
|
+
lastBuildStartedAt: 0
|
|
34
|
+
}
|
|
35
|
+
export function notifyOnRebuildPlugin(): esbuild.Plugin {
|
|
36
|
+
return {
|
|
37
|
+
name: "notify-on-rebuild",
|
|
38
|
+
setup(build) {
|
|
39
|
+
build.onStart(() => {
|
|
40
|
+
performanceStats.lastBuildStartedAt = performance.now()
|
|
41
|
+
})
|
|
42
|
+
build.onEnd(result => {
|
|
43
|
+
if (result.errors.length) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const duration = performance.now() - performanceStats.lastBuildStartedAt
|
|
47
|
+
Logger.info(`Templates built in ${chalk.green(Math.round(duration) + " ms")}.`)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function pushOnRebuildPlugin(): esbuild.Plugin {
|
|
54
|
+
return {
|
|
55
|
+
name: "push-on-rebuild",
|
|
56
|
+
setup(build) {
|
|
57
|
+
build.onEnd(result => {
|
|
58
|
+
if (result.errors.length) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only push built files
|
|
63
|
+
const { projectPath } = getCachedConfig()
|
|
64
|
+
const buildPath = path.resolve(projectPath, "build")
|
|
65
|
+
const files = fs.readdirSync(buildPath)
|
|
66
|
+
const paths = files.map(file => path.relative(projectPath, path.resolve(buildPath, file)))
|
|
67
|
+
|
|
68
|
+
pushSearchTemplate({ paths, force: false })
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
import { Logger } from "#console/logger.ts"
|
|
6
|
+
|
|
7
|
+
import { getIgnoreInstance } from "./isIgnored.ts"
|
|
8
|
+
|
|
9
|
+
export function writeFile(pathToWrite: string, data: string) {
|
|
10
|
+
if (getCachedConfig().dryRun) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
if (!fs.existsSync(path.dirname(pathToWrite))) {
|
|
14
|
+
Logger.debug(`Creating directory: ${path.dirname(pathToWrite)}`)
|
|
15
|
+
fs.mkdirSync(path.dirname(pathToWrite), { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
Logger.debug(`Writing to file: ${pathToWrite}`)
|
|
18
|
+
fs.writeFileSync(pathToWrite, data)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readFileIfExists(pathToRead: string) {
|
|
22
|
+
if (fs.existsSync(pathToRead)) {
|
|
23
|
+
return fs.readFileSync(pathToRead, "utf-8")
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listAllFiles(folder: string) {
|
|
29
|
+
const allDirents = fs.readdirSync(folder, { withFileTypes: true, recursive: true })
|
|
30
|
+
const allFiles = allDirents.filter(dirent => !dirent.isDirectory())
|
|
31
|
+
const ignoreInstance = getIgnoreInstance()
|
|
32
|
+
const filteredFiles = allFiles
|
|
33
|
+
.filter(dirent => !ignoreInstance.isIgnored(dirent))
|
|
34
|
+
.map(dirent => dirent.parentPath + "/" + dirent.name)
|
|
35
|
+
.map(file => path.relative(folder, file))
|
|
36
|
+
return {
|
|
37
|
+
allFiles: filteredFiles,
|
|
38
|
+
unfilteredFileCount: allFiles.length
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Dirent } from "fs"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import type { Ignore } from "ignore"
|
|
4
|
+
import ignore from "ignore"
|
|
5
|
+
import path from "path"
|
|
6
|
+
|
|
7
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The build directory is never ignored even if it's in the .gitignore file
|
|
11
|
+
*/
|
|
12
|
+
function isNeverIgnored(dirent: Dirent): boolean {
|
|
13
|
+
const { projectPath } = getCachedConfig()
|
|
14
|
+
const absoluteProjectPath = path.resolve(projectPath)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
dirent.parentPath.startsWith(path.join(projectPath, "build")) ||
|
|
18
|
+
dirent.parentPath.startsWith(path.join(absoluteProjectPath, "build"))
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isIgnoredImplicitly(dirent: Dirent): boolean {
|
|
23
|
+
const parentPathSections = dirent.parentPath.split("/")
|
|
24
|
+
if (
|
|
25
|
+
!dirent.isFile() ||
|
|
26
|
+
dirent.name.startsWith(".") ||
|
|
27
|
+
dirent.name.includes("node_modules") ||
|
|
28
|
+
parentPathSections.some(section => section !== "." && section.startsWith("."))
|
|
29
|
+
) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isIgnoredExplicitly(instance: Ignore, dirent: Dirent): boolean {
|
|
37
|
+
const { projectPath } = getCachedConfig()
|
|
38
|
+
const absoluteProjectPath = path.resolve(projectPath)
|
|
39
|
+
const relativePath = dirent.parentPath
|
|
40
|
+
? path.relative(absoluteProjectPath, path.join(dirent.parentPath, dirent.name))
|
|
41
|
+
: dirent.name
|
|
42
|
+
return instance.ignores(relativePath)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getIgnoreInstance() {
|
|
46
|
+
const { projectPath } = getCachedConfig()
|
|
47
|
+
|
|
48
|
+
const gitignorePath = path.join(projectPath, ".gitignore")
|
|
49
|
+
const patterns: string[] = fs.existsSync(gitignorePath)
|
|
50
|
+
? fs
|
|
51
|
+
.readFileSync(gitignorePath, "utf-8")
|
|
52
|
+
.split(/\r?\n/)
|
|
53
|
+
.filter(line => line.trim() && !line.startsWith("#"))
|
|
54
|
+
: []
|
|
55
|
+
|
|
56
|
+
const instance = ignore().add(patterns)
|
|
57
|
+
return {
|
|
58
|
+
isIgnored: (dirent: Dirent) => {
|
|
59
|
+
if (isNeverIgnored(dirent)) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return isIgnoredImplicitly(dirent) || isIgnoredExplicitly(instance, dirent)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
|
|
6
|
+
export function isModernTemplateProject() {
|
|
7
|
+
const { projectPath } = getCachedConfig()
|
|
8
|
+
const deployManifest = path.resolve(projectPath, "nosto.config.ts")
|
|
9
|
+
return fs.existsSync(deployManifest)
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
import { fetchLibraryFile } from "#api/library/fetchLibraryFile.ts"
|
|
5
|
+
import { fetchWithRetry } from "#api/retry.ts"
|
|
6
|
+
import { cleanUrl } from "#api/utils.ts"
|
|
7
|
+
import { Logger } from "#console/logger.ts"
|
|
8
|
+
|
|
9
|
+
import { writeFile } from "./filesystem.ts"
|
|
10
|
+
|
|
11
|
+
export async function loadLibrary(libraryPath: string) {
|
|
12
|
+
const filesToLoad = ["/nosto.module.js", "/nosto.module.js.map", "/nosto.d.ts"]
|
|
13
|
+
let filesFetched = 0
|
|
14
|
+
const fileCount = filesToLoad.length
|
|
15
|
+
const promises = filesToLoad.map(async filename => {
|
|
16
|
+
const data = await fetchWithRetry(fetchLibraryFile, filename)
|
|
17
|
+
filesFetched++
|
|
18
|
+
Logger.info(
|
|
19
|
+
`${chalk.green("✓")} [${filesFetched}/${fileCount}] ${chalk.blue("↓")} ${chalk.cyan(cleanUrl(filename))}`
|
|
20
|
+
)
|
|
21
|
+
return {
|
|
22
|
+
data,
|
|
23
|
+
filename
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
const files = await Promise.all(promises)
|
|
27
|
+
files.forEach(({ data, filename }) => {
|
|
28
|
+
const filePath = path.join(libraryPath, filename)
|
|
29
|
+
writeFile(filePath, data)
|
|
30
|
+
})
|
|
31
|
+
}
|